<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: CardFlow</title>
    <description>The latest articles on DEV Community by CardFlow (@cardflowng).</description>
    <link>https://dev.to/cardflowng</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4077833%2F3035343a-5dae-4b2e-8d22-17f893e1e5bb.png</url>
      <title>DEV Community: CardFlow</title>
      <link>https://dev.to/cardflowng</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cardflowng"/>
    <language>en</language>
    <item>
      <title>Designing a Privacy-Safe Gift Card Image Submission Pipeline</title>
      <dc:creator>CardFlow</dc:creator>
      <pubDate>Fri, 14 Aug 2026 15:09:44 +0000</pubDate>
      <link>https://dev.to/cardflowng/designing-a-privacy-safe-gift-card-image-submission-pipeline-655</link>
      <guid>https://dev.to/cardflowng/designing-a-privacy-safe-gift-card-image-submission-pipeline-655</guid>
      <description>&lt;p&gt;A gift card image is not an ordinary profile photo. It can contain a redeemable code, a PIN, a receipt, an email address, an order number, and location metadata from the camera. A single authorization bug can therefore expose both personal data and something that behaves like a bearer secret.&lt;/p&gt;

&lt;p&gt;This article designs the upload path as a security boundary. The examples are implementation-neutral TypeScript so the controls can be mapped to your framework, image decoder, object store, and queue.&lt;/p&gt;

&lt;p&gt;The goal is not “secure file upload” in the abstract. It is a narrower property:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Collect only the evidence needed for a decision, keep the original out of normal review paths, and make every retained copy private, attributable, and short-lived.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Start with staged disclosure
&lt;/h2&gt;

&lt;p&gt;Do not begin by asking for the entire card and receipt. Most first-pass routing decisions need only structured facts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;brand and issuing country&lt;/li&gt;
&lt;li&gt;currency and face value&lt;/li&gt;
&lt;li&gt;physical card or e-code&lt;/li&gt;
&lt;li&gt;proof type available&lt;/li&gt;
&lt;li&gt;whether the redeemable area is still covered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only request an image after those fields show that visual proof is necessary. For the first image, instruct the user to keep the code or PIN covered and exclude unrelated receipt lines. If a later step genuinely needs a live code, collect it through a separate, purpose-built secret field—not as another image in a support chat.&lt;/p&gt;

&lt;p&gt;That separation changes the failure mode. A bug in the ordinary proof viewer should not automatically reveal a spendable credential.&lt;/p&gt;

&lt;p&gt;The FTC explains why the distinction matters: someone who has the gift card number and PIN may be able to take the funds even without holding the physical card. Treat those values as secrets, not harmless text printed in a photo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Threat-model the whole path
&lt;/h2&gt;

&lt;p&gt;An upload control on the browser is useful feedback, but it is not a trust boundary. Model at least these failures:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threat&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Required control&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Secret exposure&lt;/td&gt;
&lt;td&gt;A full PIN appears in a proof image or log&lt;/td&gt;
&lt;td&gt;Staged disclosure, detection, restricted escalation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-tenant access&lt;/td&gt;
&lt;td&gt;User A changes an object ID and sees User B's proof&lt;/td&gt;
&lt;td&gt;Server-side ownership check on every read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Malicious input&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;receipt.jpg&lt;/code&gt; is HTML, a polyglot, or a decompression bomb&lt;/td&gt;
&lt;td&gt;Signature check, safe decode, byte and pixel limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metadata leakage&lt;/td&gt;
&lt;td&gt;A phone photo contains GPS or device data&lt;/td&gt;
&lt;td&gt;Decode and re-encode a review derivative without metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public storage&lt;/td&gt;
&lt;td&gt;A guessed object URL works without authentication&lt;/td&gt;
&lt;td&gt;Private buckets and mediated access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Excess retention&lt;/td&gt;
&lt;td&gt;Rejected and abandoned uploads remain indefinitely&lt;/td&gt;
&lt;td&gt;State-based deletion jobs with measurable SLAs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Insider overreach&lt;/td&gt;
&lt;td&gt;Support can browse every original&lt;/td&gt;
&lt;td&gt;Least privilege, purpose-bound access, audit events&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;OWASP's file-upload guidance recommends defense in depth: allow only business-required types, do not trust the client-supplied MIME type, generate storage names, impose size limits, store outside the web root, and scan files when appropriate. Those are the baseline, not the complete privacy design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a quarantine-to-review state machine
&lt;/h2&gt;

&lt;p&gt;A useful state model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;intent_created
  -&amp;gt; upload_quarantined
  -&amp;gt; validation_running
  -&amp;gt; review_ready | rejected
  -&amp;gt; decided
  -&amp;gt; purged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each transition should be server-controlled and idempotent. The browser never chooses &lt;code&gt;review_ready&lt;/code&gt;, and an object-store callback never decides ownership.&lt;/p&gt;

&lt;p&gt;The data flow can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;browser
  -&amp;gt; authenticated upload intent
  -&amp;gt; short-lived upload capability
  -&amp;gt; private quarantine object
  -&amp;gt; validator queue
  -&amp;gt; decode + inspect + normalize
  -&amp;gt; private review derivative
  -&amp;gt; authorized reviewer
  -&amp;gt; decision + scheduled deletion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the quarantine and review stores logically separate. The original object is untrusted input. Normal reviewers should receive the normalized derivative, not a direct link to quarantine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the upload intent the authorization root
&lt;/h2&gt;

&lt;p&gt;Create a database record before issuing an upload capability:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SubmissionIntent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;ownerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gift_card_proof&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;intent_created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;upload_quarantined&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;validation_running&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review_ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;decided&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;purged&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;quarantineKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;reviewKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The object key should be generated by the server and bound to that intent. A random key reduces collisions; it does not replace authorization.&lt;/p&gt;

&lt;p&gt;When the application serves an image, resolve the intent first and enforce the relationship:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;authorizeProofRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;intentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;submissionIntent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intentId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review_ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ownsSubmission&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ownerId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assignedReviewer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reviewQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isAssigned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ownsSubmission&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;assignedReviewer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;notFound&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returning &lt;code&gt;404&lt;/code&gt; for unauthorized object references avoids confirming that another user's submission exists. The important part is the database relationship check, not the response code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate bytes, then decode, then normalize
&lt;/h2&gt;

&lt;p&gt;Do not accept an image because its filename ends in &lt;code&gt;.jpg&lt;/code&gt; or its request header says &lt;code&gt;image/jpeg&lt;/code&gt;. A safer worker performs several independent checks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enforce a small allowlist such as JPEG, PNG, and WebP.&lt;/li&gt;
&lt;li&gt;Limit compressed bytes before buffering the whole request.&lt;/li&gt;
&lt;li&gt;Detect the file signature from the bytes.&lt;/li&gt;
&lt;li&gt;Decode with a maintained image library in a constrained worker.&lt;/li&gt;
&lt;li&gt;Limit decoded width, height, total pixels, frames, and processing time.&lt;/li&gt;
&lt;li&gt;Re-encode to one controlled output format without carrying metadata forward.&lt;/li&gt;
&lt;li&gt;Scan or sandbox the file when your risk model and tooling support it.&lt;/li&gt;
&lt;li&gt;Write the derivative under a new server-generated key.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Implementation-neutral TypeScript makes the order explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POLICY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;maxBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxPixels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/webp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateAndNormalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;QuarantinedObject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;POLICY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file_too_large&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fileInspector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prefixBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;POLICY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type_not_allowed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;probe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;imageDecoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;maxPixels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;POLICY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxPixels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxFrames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;POLICY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxPixels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pixel_limit_exceeded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;imageDecoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decodeAndEncode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;autoOrient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stripMetadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxPixels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;POLICY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxPixels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The constants are examples, not universal safe values. Choose them from the smallest image that still supports your review task and test the limits against your actual decoder.&lt;/p&gt;

&lt;p&gt;Re-encoding is valuable because it creates a controlled derivative and normally drops EXIF when configured to do so. It is not a malware guarantee. Keep the decoder isolated, patched, resource-limited, and unable to reach unrelated internal services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detect accidental secrets without pretending OCR is perfect
&lt;/h2&gt;

&lt;p&gt;OCR can help identify likely gift card codes, email addresses, phone numbers, and payment-card patterns. It should not silently rewrite evidence or make a final fraud decision.&lt;/p&gt;

&lt;p&gt;A safer result is a review gate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ExposureFinding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;possible_gift_code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;phone&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment_card&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;boundingBox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;possible_gift_code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.92&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;submissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blockForUserRedaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;quarantine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scheduleDeletion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quarantineKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PT1H&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tell the user what region appears exposed and ask for a new photo with the redeemable area physically covered. Do not send the detected text to analytics, error tracking, or a generative model. Do not store it merely because the OCR engine returned it.&lt;/p&gt;

&lt;p&gt;Some workflows may need the unmodified original for a tightly scoped investigation. Treat that as an exception: require a reason, grant time-limited access, log the actor and intent, and delete the original when the escalation closes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep object storage private
&lt;/h2&gt;

&lt;p&gt;Recommended defaults:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;block public access at the account and bucket level&lt;/li&gt;
&lt;li&gt;encrypt quarantine and review objects at rest&lt;/li&gt;
&lt;li&gt;use separate keys or access policies for the two stores&lt;/li&gt;
&lt;li&gt;issue upload and download capabilities with short expirations&lt;/li&gt;
&lt;li&gt;bind capabilities to one object key, method, size range, and content type where supported&lt;/li&gt;
&lt;li&gt;never put a signed download URL in logs, support tickets, or analytics&lt;/li&gt;
&lt;li&gt;set private responses to avoid shared caching&lt;/li&gt;
&lt;li&gt;keep original filenames only if there is a documented need; otherwise discard them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A signed URL is temporary authorization. Anyone who receives it can usually use it until it expires, so keep its lifetime short and generate it only after the database authorization check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make retention a state transition, not a policy paragraph
&lt;/h2&gt;

&lt;p&gt;“We delete uploads when no longer needed” is not testable. Put deletion deadlines in data and make the purge job observable.&lt;/p&gt;

&lt;p&gt;An example policy—not legal advice and not a universal schedule—might be:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Example deletion trigger&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Intent created, no upload&lt;/td&gt;
&lt;td&gt;Intent expiry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation rejected&lt;/td&gt;
&lt;td&gt;Quarantine cleanup within hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User abandons redaction retry&lt;/td&gt;
&lt;td&gt;Short retry-window expiry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review accepted or rejected&lt;/td&gt;
&lt;td&gt;Case-specific retention deadline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security escalation&lt;/td&gt;
&lt;td&gt;Explicit exception expiry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Store &lt;code&gt;deleteAfter&lt;/code&gt;, &lt;code&gt;retentionReason&lt;/code&gt;, and any exception owner. A daily job should delete both the database pointer and the object, then emit a content-free audit event. Alert when deletion falls behind its SLA.&lt;/p&gt;

&lt;p&gt;NIST's Privacy Framework is useful here because it treats data processing and privacy risk as an enterprise risk-management problem. In practice, minimization should affect product flow, storage design, support tools, and deletion—not only the privacy notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Log events, not evidence
&lt;/h2&gt;

&lt;p&gt;Good audit events answer who did what without copying the image or its secrets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"proof_view_granted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"intent_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"si_01J..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"actor_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"usr_01J..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assigned_reviewer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"purpose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"manual_verification"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"occurred_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-12T09:30:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid logging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;original filenames&lt;/li&gt;
&lt;li&gt;OCR text&lt;/li&gt;
&lt;li&gt;image URLs or signed query strings&lt;/li&gt;
&lt;li&gt;receipt line items&lt;/li&gt;
&lt;li&gt;user-supplied free text without filtering&lt;/li&gt;
&lt;li&gt;full request bodies from upload endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also inspect third-party observability defaults. A carefully designed application can still leak secrets if an APM agent records headers, URLs, or request payloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the privacy properties
&lt;/h2&gt;

&lt;p&gt;Unit tests for MIME types are not enough. Add adversarial and lifecycle tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;valid extension with HTML or script bytes&lt;/li&gt;
&lt;li&gt;double extension and misleading &lt;code&gt;Content-Type&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;oversized compressed file and oversized decoded dimensions&lt;/li&gt;
&lt;li&gt;animated input when only still images are allowed&lt;/li&gt;
&lt;li&gt;corrupted image that crashes or stalls the decoder&lt;/li&gt;
&lt;li&gt;EXIF GPS in the original, absent from the review derivative&lt;/li&gt;
&lt;li&gt;user A requesting user B's intent and object key&lt;/li&gt;
&lt;li&gt;expired upload and download capabilities&lt;/li&gt;
&lt;li&gt;code-like text that triggers a redaction retry&lt;/li&gt;
&lt;li&gt;secret-like content absent from logs and error events&lt;/li&gt;
&lt;li&gt;rejected, abandoned, and completed objects deleted on schedule&lt;/li&gt;
&lt;li&gt;queue retry does not create duplicate review objects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Include a recovery test too: if the worker writes the derivative and crashes before updating the database, the next run should reconcile or remove the orphan rather than retain it forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical review checklist
&lt;/h2&gt;

&lt;p&gt;Before shipping, ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Can the first decision be made without an image?&lt;/li&gt;
&lt;li&gt;Does the UI tell users to cover the redeemable area before capture?&lt;/li&gt;
&lt;li&gt;Is every upload private from the first byte?&lt;/li&gt;
&lt;li&gt;Are type, bytes, pixels, frames, and decode time constrained?&lt;/li&gt;
&lt;li&gt;Does normal review use a metadata-free derivative?&lt;/li&gt;
&lt;li&gt;Is authorization checked from the submission record on every read?&lt;/li&gt;
&lt;li&gt;Are signed URLs short-lived and excluded from logs?&lt;/li&gt;
&lt;li&gt;Can support access an original only through an audited exception?&lt;/li&gt;
&lt;li&gt;Does every state have a deletion trigger?&lt;/li&gt;
&lt;li&gt;Do tests prove cross-tenant denial and actual object deletion?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;User-facing capture guidance still matters because the safest secret is the one never uploaded. For an example of that companion layer, see CardFlow's &lt;a href="https://giftcardapp.ng/help/how-to-upload-better-gift-card-proof/" rel="noopener noreferrer"&gt;guide to uploading better gift card proof&lt;/a&gt;. The backend controls above assume the UI has already helped the user minimize the image.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP File Upload Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.nist.gov/privacy-framework/privacy-framework" rel="noopener noreferrer"&gt;NIST Privacy Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://consumer.ftc.gov/articles/avoiding-reporting-gift-card-scams" rel="noopener noreferrer"&gt;FTC: Avoiding and Reporting Gift Card Scams&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A privacy-safe image pipeline is not a single scanner or bucket setting. It is the combination of staged collection, private quarantine, controlled transformation, relationship-based authorization, purpose-bound review, and verified deletion. When those controls are expressed as states and tests, privacy becomes an engineering property instead of a promise.&lt;/p&gt;

</description>
      <category>security</category>
      <category>privacy</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
