@stapel/cdn-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/MODULE.md +124 -0
  3. package/README.md +103 -0
  4. package/dist/api/cdnApi.d.ts +122 -0
  5. package/dist/api/cdnApi.d.ts.map +1 -0
  6. package/dist/api/cdnApi.js +27 -0
  7. package/dist/api/cdnApi.js.map +1 -0
  8. package/dist/api/generated/schema.d.ts +1050 -0
  9. package/dist/api/generated/schema.d.ts.map +1 -0
  10. package/dist/api/generated/schema.js +2 -0
  11. package/dist/api/generated/schema.js.map +1 -0
  12. package/dist/api/types.d.ts +76 -0
  13. package/dist/api/types.d.ts.map +1 -0
  14. package/dist/api/types.js +2 -0
  15. package/dist/api/types.js.map +1 -0
  16. package/dist/default/ErrorAlert.d.ts +8 -0
  17. package/dist/default/ErrorAlert.d.ts.map +1 -0
  18. package/dist/default/ErrorAlert.js +26 -0
  19. package/dist/default/ErrorAlert.js.map +1 -0
  20. package/dist/default/ImageUploadField.d.ts +12 -0
  21. package/dist/default/ImageUploadField.d.ts.map +1 -0
  22. package/dist/default/ImageUploadField.js +48 -0
  23. package/dist/default/ImageUploadField.js.map +1 -0
  24. package/dist/default/MediaGalleryField.d.ts +14 -0
  25. package/dist/default/MediaGalleryField.d.ts.map +1 -0
  26. package/dist/default/MediaGalleryField.js +65 -0
  27. package/dist/default/MediaGalleryField.js.map +1 -0
  28. package/dist/default/index.d.ts +15 -0
  29. package/dist/default/index.d.ts.map +1 -0
  30. package/dist/default/index.js +13 -0
  31. package/dist/default/index.js.map +1 -0
  32. package/dist/default/phase.d.ts +17 -0
  33. package/dist/default/phase.d.ts.map +1 -0
  34. package/dist/default/phase.js +25 -0
  35. package/dist/default/phase.js.map +1 -0
  36. package/dist/flows/registry.d.ts +28 -0
  37. package/dist/flows/registry.d.ts.map +1 -0
  38. package/dist/flows/registry.js +26 -0
  39. package/dist/flows/registry.js.map +1 -0
  40. package/dist/headless/CdnProvider.d.ts +18 -0
  41. package/dist/headless/CdnProvider.d.ts.map +1 -0
  42. package/dist/headless/CdnProvider.js +14 -0
  43. package/dist/headless/CdnProvider.js.map +1 -0
  44. package/dist/headless/ImageUpload.d.ts +18 -0
  45. package/dist/headless/ImageUpload.d.ts.map +1 -0
  46. package/dist/headless/ImageUpload.js +16 -0
  47. package/dist/headless/ImageUpload.js.map +1 -0
  48. package/dist/headless/MediaUploader.d.ts +31 -0
  49. package/dist/headless/MediaUploader.d.ts.map +1 -0
  50. package/dist/headless/MediaUploader.js +30 -0
  51. package/dist/headless/MediaUploader.js.map +1 -0
  52. package/dist/headless/useUploadImage.d.ts +33 -0
  53. package/dist/headless/useUploadImage.d.ts.map +1 -0
  54. package/dist/headless/useUploadImage.js +118 -0
  55. package/dist/headless/useUploadImage.js.map +1 -0
  56. package/dist/headless/useUploadPreview.d.ts +22 -0
  57. package/dist/headless/useUploadPreview.d.ts.map +1 -0
  58. package/dist/headless/useUploadPreview.js +37 -0
  59. package/dist/headless/useUploadPreview.js.map +1 -0
  60. package/dist/headless/useUploadQueue.d.ts +83 -0
  61. package/dist/headless/useUploadQueue.d.ts.map +1 -0
  62. package/dist/headless/useUploadQueue.js +0 -0
  63. package/dist/headless/useUploadQueue.js.map +1 -0
  64. package/dist/i18n/errorsMap.d.ts +12 -0
  65. package/dist/i18n/errorsMap.d.ts.map +1 -0
  66. package/dist/i18n/errorsMap.js +22 -0
  67. package/dist/i18n/errorsMap.js.map +1 -0
  68. package/dist/i18n/es.d.ts +17 -0
  69. package/dist/i18n/es.d.ts.map +1 -0
  70. package/dist/i18n/es.js +65 -0
  71. package/dist/i18n/es.js.map +1 -0
  72. package/dist/i18n/generated/errors.es.gen.d.ts +16 -0
  73. package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
  74. package/dist/i18n/generated/errors.es.gen.js +58 -0
  75. package/dist/i18n/generated/errors.es.gen.js.map +1 -0
  76. package/dist/i18n/generated/errors.gen.d.ts +353 -0
  77. package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
  78. package/dist/i18n/generated/errors.gen.js +180 -0
  79. package/dist/i18n/generated/errors.gen.js.map +1 -0
  80. package/dist/i18n/generated/errors.ru.gen.d.ts +16 -0
  81. package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
  82. package/dist/i18n/generated/errors.ru.gen.js +58 -0
  83. package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
  84. package/dist/i18n/keys.d.ts +61 -0
  85. package/dist/i18n/keys.d.ts.map +1 -0
  86. package/dist/i18n/keys.js +110 -0
  87. package/dist/i18n/keys.js.map +1 -0
  88. package/dist/i18n/ru.d.ts +21 -0
  89. package/dist/i18n/ru.d.ts.map +1 -0
  90. package/dist/i18n/ru.js +70 -0
  91. package/dist/i18n/ru.js.map +1 -0
  92. package/dist/index.d.ts +89 -0
  93. package/dist/index.d.ts.map +1 -0
  94. package/dist/index.js +83 -0
  95. package/dist/index.js.map +1 -0
  96. package/dist/model/context.d.ts +11 -0
  97. package/dist/model/context.d.ts.map +1 -0
  98. package/dist/model/context.js +15 -0
  99. package/dist/model/context.js.map +1 -0
  100. package/dist/model/hash.d.ts +36 -0
  101. package/dist/model/hash.d.ts.map +1 -0
  102. package/dist/model/hash.js +59 -0
  103. package/dist/model/hash.js.map +1 -0
  104. package/dist/model/limits.d.ts +86 -0
  105. package/dist/model/limits.d.ts.map +1 -0
  106. package/dist/model/limits.js +163 -0
  107. package/dist/model/limits.js.map +1 -0
  108. package/dist/model/queries.d.ts +29 -0
  109. package/dist/model/queries.d.ts.map +1 -0
  110. package/dist/model/queries.js +32 -0
  111. package/dist/model/queries.js.map +1 -0
  112. package/dist/model/queryKeys.d.ts +18 -0
  113. package/dist/model/queryKeys.d.ts.map +1 -0
  114. package/dist/model/queryKeys.js +18 -0
  115. package/dist/model/queryKeys.js.map +1 -0
  116. package/dist/model/refs.d.ts +54 -0
  117. package/dist/model/refs.d.ts.map +1 -0
  118. package/dist/model/refs.js +82 -0
  119. package/dist/model/refs.js.map +1 -0
  120. package/dist/model/runtime.d.ts +40 -0
  121. package/dist/model/runtime.d.ts.map +1 -0
  122. package/dist/model/runtime.js +23 -0
  123. package/dist/model/runtime.js.map +1 -0
  124. package/dist/model/upload.d.ts +88 -0
  125. package/dist/model/upload.d.ts.map +1 -0
  126. package/dist/model/upload.js +254 -0
  127. package/dist/model/upload.js.map +1 -0
  128. package/llms.txt +83 -0
  129. package/manifest.json +601 -0
  130. package/package.json +120 -0
  131. package/src/analytics/generated/events.json +7 -0
  132. package/src/api/cdnApi.ts +187 -0
  133. package/src/api/generated/schema.ts +1052 -0
  134. package/src/api/types.ts +86 -0
  135. package/src/default/ErrorAlert.tsx +43 -0
  136. package/src/default/ImageUploadField.tsx +131 -0
  137. package/src/default/MediaGalleryField.tsx +233 -0
  138. package/src/default/index.ts +14 -0
  139. package/src/default/phase.ts +34 -0
  140. package/src/flows/registry.ts +38 -0
  141. package/src/headless/CdnProvider.tsx +19 -0
  142. package/src/headless/ImageUpload.tsx +24 -0
  143. package/src/headless/MediaUploader.tsx +43 -0
  144. package/src/headless/useUploadImage.ts +155 -0
  145. package/src/headless/useUploadPreview.ts +48 -0
  146. package/src/headless/useUploadQueue.ts +411 -0
  147. package/src/i18n/errorsMap.ts +33 -0
  148. package/src/i18n/es.ts +81 -0
  149. package/src/i18n/generated/errors.es.gen.ts +64 -0
  150. package/src/i18n/generated/errors.gen.ts +211 -0
  151. package/src/i18n/generated/errors.json +403 -0
  152. package/src/i18n/generated/errors.ru.gen.ts +64 -0
  153. package/src/i18n/keys.ts +131 -0
  154. package/src/i18n/ru.ts +85 -0
  155. package/src/index.ts +154 -0
  156. package/src/model/context.tsx +25 -0
  157. package/src/model/hash.ts +67 -0
  158. package/src/model/limits.ts +207 -0
  159. package/src/model/queries.ts +64 -0
  160. package/src/model/queryKeys.ts +23 -0
  161. package/src/model/refs.ts +95 -0
  162. package/src/model/runtime.ts +52 -0
  163. package/src/model/upload.ts +365 -0
  164. package/tsconfig.json +26 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1 @@
1
+ # @stapel/cdn-react
package/MODULE.md ADDED
@@ -0,0 +1,124 @@
1
+ # @stapel/cdn-react — module guide
2
+
3
+ Headless React pair for **stapel-cdn**. This is the human companion to the
4
+ generated `llms.txt` (agent context) and `manifest.json` (machine catalog).
5
+
6
+ ## The one thing to understand first: dedup is a step, not a side effect
7
+
8
+ Three modules in this fleet upload media, under three DIFFERENT contracts: cdn
9
+ takes a multipart POST, docs opens a session and finalises it, recordings opens
10
+ a session with a size ceiling. They were never one algorithm wearing three hats,
11
+ and this package does not pretend otherwise — it implements **cdn's** contract.
12
+ The two genuinely contract-free bones the other two shared were extracted into
13
+ `@stapel/core` instead (`putToForeignOrigin`, `useObjectUrlPreview`), and only
14
+ the second of them appears here: there is no foreign origin in this contract to
15
+ PUT to.
16
+
17
+ What cdn's contract has, and the others do not, is a content-addressed store
18
+ with a public "do you already have these bytes?" question. So the flow is:
19
+
20
+ ```
21
+ validate ─┬─ refuse (client-side mirror, cdn's own error codes)
22
+
23
+ └─ hash ── GET file/exists/?file_hash=… ─┬─ HIT → done, no POST
24
+
25
+ └─ MISS → POST multipart
26
+ └─ poll for variants
27
+ ```
28
+
29
+ `test/dedup.test.ts` asserts the HIT leg by **counting requests**, which is the
30
+ only assertion that cannot be satisfied by a flow that merely looks right.
31
+
32
+ ### What a hit promises, and what a miss does not
33
+
34
+ A hit means: these bytes are stored, owned by this caller, as the asset type
35
+ this upload would produce. All three conditions are checked, because
36
+ `file/exists/` answers about any object with those bytes — the same file stored
37
+ earlier as a video, or as an avatar rather than a listing photo, is not the row
38
+ the POST would return, and short-circuiting on it would hand a composer a
39
+ reference to the wrong thing.
40
+
41
+ A miss promises nothing about the server. `file/exists/` filters on
42
+ `uploaded_by=request.user` **unconditionally**, while the upload views honour
43
+ `STAPEL_CDN["DEDUP_SCOPE"]` (default `"owner"`, optionally `"global"`). Under a
44
+ global scope the POST can still answer 200 "already exists". `deduped` therefore
45
+ reports what this client observed, never a claim about what the server did.
46
+
47
+ ## Layers
48
+
49
+ | Layer | What lives there |
50
+ |---|---|
51
+ | `api/generated/schema.ts` | `pnpm gen:api` from stapel-cdn's `docs/schema.json`. Never hand-edited. |
52
+ | `api/types.ts` | The public projection, with two documented widenings (see below). |
53
+ | `api/cdnApi.ts` | One method per endpoint a browser may call. The one legal home of a path string. |
54
+ | `model/limits.ts` | The client-side mirror of `STAPEL_CDN`'s ceilings — configurable, because they are settings. |
55
+ | `model/hash.ts` | SHA-256 over the file, and the honest answer when the platform cannot compute one. |
56
+ | `model/upload.ts` | The flow. No React. |
57
+ | `model/refs.ts` | `<type>/<hash>` and the single conversion to `@stapel/image`'s ladder. |
58
+ | `model/queries.ts` | `useCdnRef` — the pair's one cached read. |
59
+ | `headless/` | `useUploadQueue` / `useUploadImage` and their render-prop components. |
60
+ | `default/` | The antd skin, behind its own subpath. |
61
+ | `i18n/` | Keys + the generated error bundles + authored ru/es. |
62
+ | `flows/registry.ts` | The zero-flow shim: stapel-cdn annotates no `@flow_step`. |
63
+
64
+ ## Two widenings of the generated types, and why
65
+
66
+ 1. **`Image.type` is a string here, not the generated `"avatar"` enum.** The
67
+ schema builds `TypeEnum` from `STAPEL_CDN["ASSET_TYPES"]`, whose library
68
+ default is `("avatar",)`. `POST /upload/image/` never reads that setting: it
69
+ writes `type="product"` unconditionally. So on a default deployment the
70
+ general intake returns a value the generated union does not contain — and
71
+ `POST /images/product/upload/` (which DOES validate against `ASSET_TYPES`)
72
+ answers 400 for the very type the other endpoint just stored. Both facts are
73
+ upstream's; the pair reflects them rather than pretending the enum is closed.
74
+ 2. **`FileExistsResponse.type`** is `string | null` on the wire. `CdnFileKind`
75
+ names the three values the view can produce without narrowing the parsed
76
+ value, so a fourth kind added upstream is not a runtime cast that lies.
77
+
78
+ ## The progress question, answered once
79
+
80
+ There is no honest byte-percentage available. `fetch` cannot observe
81
+ request-body progress through the injected client, and `SubtleCrypto.digest`
82
+ takes the whole buffer and reports nothing until it is done. The two ways to
83
+ show a moving bar anyway are to fork the transport onto `XMLHttpRequest` — a
84
+ second transport with its own bearer/refresh/verification/error handling, i.e.
85
+ the duplication this package exists to end — or to animate a number nobody
86
+ measured. `UploadPhase` names the step instead, and the skins render an
87
+ indeterminate indicator for the two steps that take real time.
88
+
89
+ ## The gates a composer reads
90
+
91
+ ```ts
92
+ bag.canAdd // available | blocked{ cdn.upload.blocked.full, {max} }
93
+ bag.settled // available | blocked{ …blocked.pending } | blocked{ …blocked.failed }
94
+ ```
95
+
96
+ Both are `ActionAvailability` rather than booleans because a Save button that is
97
+ off has to be able to say WHICH of the two reasons it is. `bag.refs` contains
98
+ only settled references, so it is never a promise about bytes that are not
99
+ there.
100
+
101
+ ## Upstream notes (recorded, not worked around)
102
+
103
+ - **No public read-by-reference.** `file/exists/` is owner-scoped, so nothing in
104
+ this contract resolves a stranger's `<type>/<hash>` to URLs. A storefront
105
+ renders a listing's photos from the listings API's own payload.
106
+ - **`refs/sync/` is `IsServiceRequest`** and unreachable from a browser: the
107
+ consuming module's server syncs references. Not on this pair's API surface.
108
+ - **`GET /images/{type}/random/` is `IsStaffUser`** — an admin convenience, not
109
+ a storefront operation. Also not on the surface.
110
+ - **413 is undeclared for the image endpoints.** `upload/video/` lists it;
111
+ `upload/image/`, `upload/avatar/` and `images/{type}/upload/` describe the
112
+ ceiling in prose and return 413 without declaring it in `responses`. The pair
113
+ handles it identically for all four.
114
+ - **The variant ladder has no readiness callback.** Polling `file/exists/` is the
115
+ only way to learn that the background task finished, so the wait is bounded
116
+ and its exhaustion is a stated outcome (`variantsReady: false`), not a hang.
117
+
118
+ ## Testing
119
+
120
+ `test/` mocks the WIRE, never the module: every request goes through the real
121
+ `StapelClient`, the bodies are the ones stapel-cdn's serializers render, and the
122
+ SHA-256 is really computed — which is what makes the request-count assertions
123
+ meaningful. jsdom's gaps (`Blob.prototype.arrayBuffer`, `URL.createObjectURL`)
124
+ are filled minimally in `test/vitest.setup.ts` and nowhere else.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # @stapel/cdn-react
2
+
3
+ Headless React pair for **stapel-cdn** (frontend-standard §2). Business + state
4
+ only in the main entry, zero visual opinion — any design layers on top. Built on
5
+ `@stapel/core` (typed client + `StapelApiError` envelope, token refresh,
6
+ verification-403 interception, i18n engine, TanStack Query).
7
+
8
+ It uploads media the way stapel-cdn wants it uploaded, and its first move is not
9
+ to upload: it hashes the file and asks whether the CDN already has these bytes.
10
+ When it does, nothing is sent.
11
+
12
+ ## Install
13
+
14
+ ```
15
+ pnpm add @stapel/cdn-react @stapel/core @tanstack/react-query react
16
+ ```
17
+
18
+ ## Wire the app once
19
+
20
+ ```tsx
21
+ import { CdnProvider, createCdnRuntime } from "@stapel/cdn-react";
22
+
23
+ const cdn = createCdnRuntime({ baseUrl: "/cdn/api/v1/" });
24
+
25
+ <StapelConfigProvider config={{ clients: { cdn: cdn.client } }}>
26
+ <CdnProvider runtime={cdn}>{app}</CdnProvider>
27
+ </StapelConfigProvider>;
28
+ ```
29
+
30
+ ## A gallery (what a listing composer uses)
31
+
32
+ ```tsx
33
+ import { MediaUploader } from "@stapel/cdn-react";
34
+
35
+ <MediaUploader max={10} onRefsChange={(refs) => form.set("images_draft", refs)}>
36
+ {({ items, refs, canAdd, settled, add, remove, reorder }) => …}
37
+ </MediaUploader>;
38
+ ```
39
+
40
+ `refs` is the list of `<type>/<hash>` strings to store, **in display order**.
41
+ `canAdd` and `settled` are `ActionAvailability`, so the Add button and the Save
42
+ button can each say why they are off — "this gallery holds at most 10 photos",
43
+ "wait for the uploads to finish", "retry the photos that failed".
44
+
45
+ The antd skin over the same bag is one import away:
46
+
47
+ ```tsx
48
+ import { MediaGalleryField, ImageUploadField } from "@stapel/cdn-react/default";
49
+ ```
50
+
51
+ ## One slot (avatar, cover)
52
+
53
+ ```tsx
54
+ const { upload, previewUrl, phase, error } = useUploadImage({
55
+ target: { kind: "avatar" },
56
+ });
57
+ ```
58
+
59
+ ## What the flow does, in order
60
+
61
+ ```
62
+ validate ─┬─ refuse (client-side mirror of the deployment's own ceilings)
63
+ └─ hash ── file/exists/ ─┬─ HIT → done, ZERO bytes sent
64
+ └─ MISS → multipart POST → wait for variants
65
+ ```
66
+
67
+ ## Three things worth knowing before you use it
68
+
69
+ **There is no progress percentage, on purpose.** `fetch` cannot observe how much
70
+ of a request body has gone out, and `crypto.subtle.digest` reports nothing
71
+ mid-digest. So the bag names the **phase** — hashing, checking, uploading,
72
+ preparing previews — and a skin shows an indeterminate indicator. Getting a real
73
+ number would mean re-implementing the upload on `XMLHttpRequest`, with its own
74
+ copy of the client's auth, refresh and error handling. A bar that is measured
75
+ beats a bar that moves.
76
+
77
+ **The pre-check can be skipped and that is never fatal.** `file/exists/` needs
78
+ `IsAuthenticated`, while the upload endpoints take `IsNotAnonymousUser` — so a
79
+ guest can upload but cannot pre-check. A page served over plain `http://` has no
80
+ `crypto.subtle` at all. Both fall through to the POST (the server deduplicates
81
+ on its own side regardless) and report `dedupSkipped` so a skin can explain it.
82
+
83
+ **Reading somebody else's reference is not possible here, and that is upstream's
84
+ gap, not this pair's.** `file/exists/` filters on `uploaded_by=request.user`
85
+ unconditionally, so `useCdnRef` resolves the caller's OWN references — which is
86
+ what a reopened draft needs and what a buyer looking at a seller's gallery does
87
+ not. stapel-cdn exposes no public read-by-reference endpoint; a storefront
88
+ renders a listing's photos from what the listings API hands it. Recorded here
89
+ rather than worked around with a URL convention this pair would have invented.
90
+
91
+ ## Layers
92
+
93
+ `api/` (generated schema + the typed operations) → `model/` (the flow, the
94
+ limits mirror, the reference, the one cached read) → `headless/` (the bags and
95
+ their render-prop components) → `i18n/`. `default/` is the antd skin behind its
96
+ own subpath. `MODULE.md` has the full map.
97
+
98
+ ## Locales
99
+
100
+ English is inline. `./i18n/ru` and `./i18n/es` are opt-in subpaths. stapel-cdn
101
+ ships no `translations/` directory at all, so the 11 error keys it owns are
102
+ authored in this package until upstream localizes them; the 42 cross-cutting
103
+ keys come from stapel-core's catalogue through `gen:errors`.
@@ -0,0 +1,122 @@
1
+ import type { StapelClient } from "@stapel/core";
2
+ import type { CdnFileExistsResponse, CdnFileUploadResponse, CdnImageUploadResponse, CdnVideoUploadResponse } from "./types.js";
3
+ /**
4
+ * The pair's typed operation surface — one method per stapel-cdn endpoint a
5
+ * browser may call, bound to the injected {@link StapelClient} (the per-module
6
+ * override seam of frontend-standard §7.2). Paths are relative to the
7
+ * runtime's `baseUrl` (`/cdn/api/v1/`).
8
+ *
9
+ * ── The two endpoints that are NOT here, and why ───────────────────────────
10
+ *
11
+ * `POST /refs/sync/` is `IsServiceRequest`: it is how one BACKEND tells the
12
+ * CDN that an entity now references a set of media, and a browser cannot
13
+ * authenticate as a service. A pair that exposed it would invite a screen to
14
+ * call something that can only answer 403 (`stapel_cdn/views.py`, RefSyncView).
15
+ * Reference bookkeeping is the consuming module's server-side job — the
16
+ * storefront's part is to hand `images_draft` to stapel-listings and let it
17
+ * sync.
18
+ *
19
+ * `GET /images/{image_type}/random/` is `IsStaffUser` and exists to let an
20
+ * admin UI grab a test image. Same argument.
21
+ *
22
+ * Both stay in the generated schema (and therefore in `manifest.json`, which
23
+ * lists the whole contract), so nothing is hidden; they are simply not this
24
+ * pair's surface.
25
+ *
26
+ * ── Upload is a POST of `multipart/form-data`, not a presigned PUT ─────────
27
+ *
28
+ * This is the whole reason `@stapel/core`'s `putToForeignOrigin` does not
29
+ * appear anywhere in this package. That primitive exists for the docs and
30
+ * recordings contracts, which open a session and hand back a URL at an object
31
+ * store. stapel-cdn takes the bytes itself, through the same authenticated
32
+ * origin as every other call, so the injected client IS the right instrument
33
+ * here and there is no foreign origin to PUT to. Three upload implementations,
34
+ * three contracts (spec §1.6) — the bones core extracted are the ones the
35
+ * OTHER two share.
36
+ */
37
+ export interface CdnApi {
38
+ readonly client: StapelClient;
39
+ /**
40
+ * Has the CDN already got these bytes?
41
+ *
42
+ * The dedup pre-check, and the pair's only read. Answers `200` either way:
43
+ * `{exists: false, type: null, file: null}` is a successful answer, not an
44
+ * error — "we asked and there is none" (`FileExistsView`).
45
+ *
46
+ * OWNER-SCOPED, ALWAYS. The view filters on `uploaded_by=request.user`
47
+ * unconditionally, unlike the upload paths, which honour
48
+ * `STAPEL_CDN["DEDUP_SCOPE"]`. Two consequences the flow in `model/upload.ts`
49
+ * is built around: (a) a miss here does NOT mean the upload will store new
50
+ * bytes — under `DEDUP_SCOPE: "global"` the POST may still answer "already
51
+ * exists"; (b) a buyer cannot resolve a seller's reference through this
52
+ * endpoint, so it is a re-open-your-own-draft read, never a public one.
53
+ *
54
+ * `IsAuthenticated | IsServiceRequest` — note that this is STRICTER than the
55
+ * upload endpoints, which take `IsNotAnonymousUser` (a guest identity is
56
+ * enough). A guest can therefore upload but not pre-check, which is why a
57
+ * 401 here is a skipped optimisation rather than a failed upload.
58
+ */
59
+ fileExists(fileHash: string, options?: {
60
+ readonly signal?: AbortSignal;
61
+ }): Promise<CdnFileExistsResponse>;
62
+ /**
63
+ * `POST /upload/image/` — the general image intake (`IsNotAnonymousUser`).
64
+ *
65
+ * Answers `201` for stored bytes and `200` for a server-side dedup hit, with
66
+ * the SAME body either way. The pair does not report which: `StapelClient`
67
+ * resolves a body, not a status, and the only in-body difference is the
68
+ * English `message` string, which is not a contract. Dedup is therefore
69
+ * reported from the pre-check (which is a contract) and from nowhere else —
70
+ * see `model/upload.ts`.
71
+ *
72
+ * The stored row's `type` is `"product"`, hardcoded in the view.
73
+ */
74
+ uploadImage(file: File, options?: {
75
+ readonly signal?: AbortSignal;
76
+ }): Promise<CdnImageUploadResponse>;
77
+ /**
78
+ * `POST /upload/avatar/` — the same intake with `type="avatar"`, and the one
79
+ * upload endpoint that requires a real `IsAuthenticated` principal. This is
80
+ * what `profiles-react`'s `useSetAvatar` calls (today through its own
81
+ * documented stopgap, `api/cdnAvatarApi.ts`, which this pair exists to
82
+ * replace).
83
+ */
84
+ uploadAvatar(file: File, options?: {
85
+ readonly signal?: AbortSignal;
86
+ }): Promise<CdnImageUploadResponse>;
87
+ /**
88
+ * `POST /images/{image_type}/upload/` — an image stored under a caller-named
89
+ * type, validated against `STAPEL_CDN["ASSET_TYPES"]`.
90
+ *
91
+ * Refuses `error.400.invalid_image_type` for a type this deployment does not
92
+ * declare. Note the asymmetry with {@link uploadImage}, which writes
93
+ * `"product"` without consulting that setting at all: on a default
94
+ * deployment `POST /images/product/upload/` is a 400 while
95
+ * `POST /upload/image/` happily stores a `product` row. A host that wants
96
+ * `product` addressable by name adds it to `ASSET_TYPES`.
97
+ */
98
+ uploadTypedImage(imageType: string, file: File, options?: {
99
+ readonly signal?: AbortSignal;
100
+ }): Promise<CdnImageUploadResponse>;
101
+ /**
102
+ * `POST /upload/video/`. Typed and callable; this pair ships no hook and no
103
+ * widget over it (the storefront MVP is images), so a host that needs video
104
+ * today calls it directly rather than waiting for a version of the pair that
105
+ * has the queue for it. Variants are not generated yet upstream — the
106
+ * response's `is_processed` stays false and the ladder is empty.
107
+ */
108
+ uploadVideo(file: File, options?: {
109
+ readonly signal?: AbortSignal;
110
+ }): Promise<CdnVideoUploadResponse>;
111
+ /**
112
+ * `POST /upload/file/` — documents and archives. Same status as
113
+ * {@link uploadVideo}: typed, callable, no hook. Its allowlist is a MIME
114
+ * allowlist as well as an extension one, and deliberately excludes
115
+ * `application/octet-stream`.
116
+ */
117
+ uploadFile(file: File, options?: {
118
+ readonly signal?: AbortSignal;
119
+ }): Promise<CdnFileUploadResponse>;
120
+ }
121
+ export declare function createCdnApi(client: StapelClient): CdnApi;
122
+ //# sourceMappingURL=cdnApi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdnApi.d.ts","sourceRoot":"","sources":["../../src/api/cdnApi.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EACV,qBAAqB,EACrB,qBAAqB,EACrB,sBAAsB,EACtB,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAE9B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,UAAU,CACR,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAC1C,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAElC;;;;;;;;;;;OAWG;IACH,WAAW,CACT,IAAI,EAAE,IAAI,EACV,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAC1C,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAEnC;;;;;;OAMG;IACH,YAAY,CACV,IAAI,EAAE,IAAI,EACV,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAC1C,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAEnC;;;;;;;;;;OAUG;IACH,gBAAgB,CACd,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,IAAI,EACV,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAC1C,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAEnC;;;;;;OAMG;IACH,WAAW,CACT,IAAI,EAAE,IAAI,EACV,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAC1C,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAEnC;;;;;OAKG;IACH,UAAU,CACR,IAAI,EAAE,IAAI,EACV,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAC1C,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACnC;AAmBD,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CA6BzD"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The one multipart body every upload endpoint takes: a single `file` part.
3
+ * `FormData` is a `BodyInit`, so `StapelClient` sends it verbatim and lets the
4
+ * browser write the `Content-Type` boundary — setting that header by hand is
5
+ * the classic way to make a multipart POST unparseable server-side.
6
+ */
7
+ function filePart(file) {
8
+ const form = new FormData();
9
+ form.append("file", file);
10
+ return form;
11
+ }
12
+ const signalOf = (options) => options?.signal !== undefined ? { signal: options.signal } : {};
13
+ export function createCdnApi(client) {
14
+ return {
15
+ client,
16
+ fileExists: (fileHash, options) => client.get("/file/exists/", {
17
+ query: { file_hash: fileHash },
18
+ ...signalOf(options),
19
+ }),
20
+ uploadImage: (file, options) => client.post("/upload/image/", filePart(file), signalOf(options)),
21
+ uploadAvatar: (file, options) => client.post("/upload/avatar/", filePart(file), signalOf(options)),
22
+ uploadTypedImage: (imageType, file, options) => client.post(`/images/${encodeURIComponent(imageType)}/upload/`, filePart(file), signalOf(options)),
23
+ uploadVideo: (file, options) => client.post("/upload/video/", filePart(file), signalOf(options)),
24
+ uploadFile: (file, options) => client.post("/upload/file/", filePart(file), signalOf(options)),
25
+ };
26
+ }
27
+ //# sourceMappingURL=cdnApi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdnApi.js","sourceRoot":"","sources":["../../src/api/cdnApi.ts"],"names":[],"mappings":"AA4IA;;;;;GAKG;AACH,SAAS,QAAQ,CAAC,IAAU;IAC1B,MAAM,IAAI,GAAG,IAAI,QAAQ,EAAE,CAAC;IAC5B,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC1B,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,QAAQ,GAAG,CAAC,OAEjB,EAA4B,EAAE,CAC7B,OAAO,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAElE,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,OAAO;QACL,MAAM;QAEN,UAAU,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,CAChC,MAAM,CAAC,GAAG,CAAC,eAAe,EAAE;YAC1B,KAAK,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE;YAC9B,GAAG,QAAQ,CAAC,OAAO,CAAC;SACrB,CAAC;QAEJ,WAAW,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,CAC7B,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;QAElE,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,CAC9B,MAAM,CAAC,IAAI,CAAC,iBAAiB,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;QAEnE,gBAAgB,EAAE,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,CAC7C,MAAM,CAAC,IAAI,CACT,WAAW,kBAAkB,CAAC,SAAS,CAAC,UAAU,EAClD,QAAQ,CAAC,IAAI,CAAC,EACd,QAAQ,CAAC,OAAO,CAAC,CAClB;QAEH,WAAW,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,CAC7B,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;QAElE,UAAU,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,CAC5B,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;KAClE,CAAC;AACJ,CAAC"}