@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/package.json ADDED
@@ -0,0 +1,120 @@
1
+ {
2
+ "name": "@stapel/cdn-react",
3
+ "version": "0.1.0",
4
+ "description": "Headless React pair for stapel-cdn: a typed media-upload client whose flow is dedup-first — SHA-256 the bytes, ask file/exists/, and skip the POST entirely when the CDN already holds them. A phase-shaped upload bag (validate → hash → check → upload → variants), an ordered queue with per-item cancel/retry/remove and a max, object-URL previews with the revoke built in (@stapel/core's useObjectUrlPreview), and the opaque `<type>/<hash>` reference every consuming module actually stores. Client-side limits mirror the backend's own declared ceilings and never refuse what the server would accept. Zero visual opinion in the main entry; an opt-in /default subpath ships the antd skin.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/usestapel/stapel-react.git",
9
+ "directory": "packages/cdn-react"
10
+ },
11
+ "type": "module",
12
+ "sideEffects": false,
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ },
20
+ "./default": {
21
+ "types": "./dist/default/index.d.ts",
22
+ "default": "./dist/default/index.js"
23
+ },
24
+ "./i18n/ru": {
25
+ "types": "./dist/i18n/ru.d.ts",
26
+ "default": "./dist/i18n/ru.js"
27
+ },
28
+ "./i18n/es": {
29
+ "types": "./dist/i18n/es.d.ts",
30
+ "default": "./dist/i18n/es.js"
31
+ },
32
+ "./manifest": "./manifest.json",
33
+ "./manifest.json": "./manifest.json",
34
+ "./llms.txt": "./llms.txt",
35
+ "./package.json": "./package.json"
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src",
40
+ "tsconfig.json",
41
+ "README.md",
42
+ "MODULE.md",
43
+ "CHANGELOG.md",
44
+ "manifest.json",
45
+ "llms.txt"
46
+ ],
47
+ "scripts": {
48
+ "build": "tsc -p tsconfig.json",
49
+ "test": "tsc -p tsconfig.demo.json && vitest run --exclude test/prodBundlePurity.test.ts",
50
+ "test:pack": "vitest run test/prodBundlePurity.test.ts",
51
+ "lint": "eslint .",
52
+ "size": "size-limit"
53
+ },
54
+ "size-limit": [
55
+ {
56
+ "name": "index — headless pair (client + dedup-first upload flow + queue), no antd",
57
+ "path": "dist/index.js",
58
+ "limit": "8 KB"
59
+ },
60
+ {
61
+ "name": "default — the antd upload skin must stay out of the main bundle",
62
+ "path": "dist/default/index.js",
63
+ "limit": "8 KB"
64
+ },
65
+ {
66
+ "name": "i18n/ru — the Russian bundle is opt-in, not carried by the main entry",
67
+ "path": "dist/i18n/ru.js",
68
+ "limit": "3 KB"
69
+ },
70
+ {
71
+ "name": "i18n/es — the Spanish bundle is opt-in, not carried by the main entry",
72
+ "path": "dist/i18n/es.js",
73
+ "limit": "2.5 KB"
74
+ }
75
+ ],
76
+ "peerDependencies": {
77
+ "@stapel/core": ">=0.15.0 <1.0.0",
78
+ "@stapel/image": ">=0.2.0",
79
+ "@stapel/tokens-antd": ">=0.5.0",
80
+ "@tanstack/react-query": "^5.0.0",
81
+ "antd": ">=5.20.0 <7",
82
+ "react": ">=19"
83
+ },
84
+ "peerDependenciesMeta": {
85
+ "@stapel/image": {
86
+ "optional": true
87
+ },
88
+ "@stapel/tokens-antd": {
89
+ "optional": true
90
+ },
91
+ "antd": {
92
+ "optional": true
93
+ }
94
+ },
95
+ "devDependencies": {
96
+ "@size-limit/preset-small-lib": "^11.2.0",
97
+ "@stapel/core": "workspace:^",
98
+ "@stapel/image": "workspace:^",
99
+ "@stapel/showcase": "workspace:^",
100
+ "@stapel/tokens": "workspace:^",
101
+ "@stapel/tokens-antd": "workspace:^",
102
+ "@tanstack/react-query": "^5.81.0",
103
+ "@testing-library/react": "^16.3.0",
104
+ "@types/react": "^19.1.0",
105
+ "@types/react-dom": "^19.1.0",
106
+ "antd": "^6.5.0",
107
+ "jsdom": "^26.1.0",
108
+ "react": "^19.1.0",
109
+ "react-dom": "^19.1.0",
110
+ "size-limit": "^11.2.0",
111
+ "typescript": "^5.8.3",
112
+ "vitest": "^3.2.4"
113
+ },
114
+ "engines": {
115
+ "node": ">=22"
116
+ },
117
+ "publishConfig": {
118
+ "access": "public"
119
+ }
120
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "$generated": "by scripts/gen-events.mjs — do not edit; drift-gated (pnpm gen:events:check)",
3
+ "package": "@stapel/cdn-react",
4
+ "version": "0.1.0",
5
+ "defined": [],
6
+ "flows": []
7
+ }
@@ -0,0 +1,187 @@
1
+ import type { StapelClient } from "@stapel/core";
2
+ import type {
3
+ CdnFileExistsResponse,
4
+ CdnFileUploadResponse,
5
+ CdnImageUploadResponse,
6
+ CdnVideoUploadResponse,
7
+ } from "./types.js";
8
+
9
+ /**
10
+ * The pair's typed operation surface — one method per stapel-cdn endpoint a
11
+ * browser may call, bound to the injected {@link StapelClient} (the per-module
12
+ * override seam of frontend-standard §7.2). Paths are relative to the
13
+ * runtime's `baseUrl` (`/cdn/api/v1/`).
14
+ *
15
+ * ── The two endpoints that are NOT here, and why ───────────────────────────
16
+ *
17
+ * `POST /refs/sync/` is `IsServiceRequest`: it is how one BACKEND tells the
18
+ * CDN that an entity now references a set of media, and a browser cannot
19
+ * authenticate as a service. A pair that exposed it would invite a screen to
20
+ * call something that can only answer 403 (`stapel_cdn/views.py`, RefSyncView).
21
+ * Reference bookkeeping is the consuming module's server-side job — the
22
+ * storefront's part is to hand `images_draft` to stapel-listings and let it
23
+ * sync.
24
+ *
25
+ * `GET /images/{image_type}/random/` is `IsStaffUser` and exists to let an
26
+ * admin UI grab a test image. Same argument.
27
+ *
28
+ * Both stay in the generated schema (and therefore in `manifest.json`, which
29
+ * lists the whole contract), so nothing is hidden; they are simply not this
30
+ * pair's surface.
31
+ *
32
+ * ── Upload is a POST of `multipart/form-data`, not a presigned PUT ─────────
33
+ *
34
+ * This is the whole reason `@stapel/core`'s `putToForeignOrigin` does not
35
+ * appear anywhere in this package. That primitive exists for the docs and
36
+ * recordings contracts, which open a session and hand back a URL at an object
37
+ * store. stapel-cdn takes the bytes itself, through the same authenticated
38
+ * origin as every other call, so the injected client IS the right instrument
39
+ * here and there is no foreign origin to PUT to. Three upload implementations,
40
+ * three contracts (spec §1.6) — the bones core extracted are the ones the
41
+ * OTHER two share.
42
+ */
43
+ export interface CdnApi {
44
+ readonly client: StapelClient;
45
+
46
+ /**
47
+ * Has the CDN already got these bytes?
48
+ *
49
+ * The dedup pre-check, and the pair's only read. Answers `200` either way:
50
+ * `{exists: false, type: null, file: null}` is a successful answer, not an
51
+ * error — "we asked and there is none" (`FileExistsView`).
52
+ *
53
+ * OWNER-SCOPED, ALWAYS. The view filters on `uploaded_by=request.user`
54
+ * unconditionally, unlike the upload paths, which honour
55
+ * `STAPEL_CDN["DEDUP_SCOPE"]`. Two consequences the flow in `model/upload.ts`
56
+ * is built around: (a) a miss here does NOT mean the upload will store new
57
+ * bytes — under `DEDUP_SCOPE: "global"` the POST may still answer "already
58
+ * exists"; (b) a buyer cannot resolve a seller's reference through this
59
+ * endpoint, so it is a re-open-your-own-draft read, never a public one.
60
+ *
61
+ * `IsAuthenticated | IsServiceRequest` — note that this is STRICTER than the
62
+ * upload endpoints, which take `IsNotAnonymousUser` (a guest identity is
63
+ * enough). A guest can therefore upload but not pre-check, which is why a
64
+ * 401 here is a skipped optimisation rather than a failed upload.
65
+ */
66
+ fileExists(
67
+ fileHash: string,
68
+ options?: { readonly signal?: AbortSignal }
69
+ ): Promise<CdnFileExistsResponse>;
70
+
71
+ /**
72
+ * `POST /upload/image/` — the general image intake (`IsNotAnonymousUser`).
73
+ *
74
+ * Answers `201` for stored bytes and `200` for a server-side dedup hit, with
75
+ * the SAME body either way. The pair does not report which: `StapelClient`
76
+ * resolves a body, not a status, and the only in-body difference is the
77
+ * English `message` string, which is not a contract. Dedup is therefore
78
+ * reported from the pre-check (which is a contract) and from nowhere else —
79
+ * see `model/upload.ts`.
80
+ *
81
+ * The stored row's `type` is `"product"`, hardcoded in the view.
82
+ */
83
+ uploadImage(
84
+ file: File,
85
+ options?: { readonly signal?: AbortSignal }
86
+ ): Promise<CdnImageUploadResponse>;
87
+
88
+ /**
89
+ * `POST /upload/avatar/` — the same intake with `type="avatar"`, and the one
90
+ * upload endpoint that requires a real `IsAuthenticated` principal. This is
91
+ * what `profiles-react`'s `useSetAvatar` calls (today through its own
92
+ * documented stopgap, `api/cdnAvatarApi.ts`, which this pair exists to
93
+ * replace).
94
+ */
95
+ uploadAvatar(
96
+ file: File,
97
+ options?: { readonly signal?: AbortSignal }
98
+ ): Promise<CdnImageUploadResponse>;
99
+
100
+ /**
101
+ * `POST /images/{image_type}/upload/` — an image stored under a caller-named
102
+ * type, validated against `STAPEL_CDN["ASSET_TYPES"]`.
103
+ *
104
+ * Refuses `error.400.invalid_image_type` for a type this deployment does not
105
+ * declare. Note the asymmetry with {@link uploadImage}, which writes
106
+ * `"product"` without consulting that setting at all: on a default
107
+ * deployment `POST /images/product/upload/` is a 400 while
108
+ * `POST /upload/image/` happily stores a `product` row. A host that wants
109
+ * `product` addressable by name adds it to `ASSET_TYPES`.
110
+ */
111
+ uploadTypedImage(
112
+ imageType: string,
113
+ file: File,
114
+ options?: { readonly signal?: AbortSignal }
115
+ ): Promise<CdnImageUploadResponse>;
116
+
117
+ /**
118
+ * `POST /upload/video/`. Typed and callable; this pair ships no hook and no
119
+ * widget over it (the storefront MVP is images), so a host that needs video
120
+ * today calls it directly rather than waiting for a version of the pair that
121
+ * has the queue for it. Variants are not generated yet upstream — the
122
+ * response's `is_processed` stays false and the ladder is empty.
123
+ */
124
+ uploadVideo(
125
+ file: File,
126
+ options?: { readonly signal?: AbortSignal }
127
+ ): Promise<CdnVideoUploadResponse>;
128
+
129
+ /**
130
+ * `POST /upload/file/` — documents and archives. Same status as
131
+ * {@link uploadVideo}: typed, callable, no hook. Its allowlist is a MIME
132
+ * allowlist as well as an extension one, and deliberately excludes
133
+ * `application/octet-stream`.
134
+ */
135
+ uploadFile(
136
+ file: File,
137
+ options?: { readonly signal?: AbortSignal }
138
+ ): Promise<CdnFileUploadResponse>;
139
+ }
140
+
141
+ /**
142
+ * The one multipart body every upload endpoint takes: a single `file` part.
143
+ * `FormData` is a `BodyInit`, so `StapelClient` sends it verbatim and lets the
144
+ * browser write the `Content-Type` boundary — setting that header by hand is
145
+ * the classic way to make a multipart POST unparseable server-side.
146
+ */
147
+ function filePart(file: File): FormData {
148
+ const form = new FormData();
149
+ form.append("file", file);
150
+ return form;
151
+ }
152
+
153
+ const signalOf = (options?: {
154
+ readonly signal?: AbortSignal;
155
+ }): { signal?: AbortSignal } =>
156
+ options?.signal !== undefined ? { signal: options.signal } : {};
157
+
158
+ export function createCdnApi(client: StapelClient): CdnApi {
159
+ return {
160
+ client,
161
+
162
+ fileExists: (fileHash, options) =>
163
+ client.get("/file/exists/", {
164
+ query: { file_hash: fileHash },
165
+ ...signalOf(options),
166
+ }),
167
+
168
+ uploadImage: (file, options) =>
169
+ client.post("/upload/image/", filePart(file), signalOf(options)),
170
+
171
+ uploadAvatar: (file, options) =>
172
+ client.post("/upload/avatar/", filePart(file), signalOf(options)),
173
+
174
+ uploadTypedImage: (imageType, file, options) =>
175
+ client.post(
176
+ `/images/${encodeURIComponent(imageType)}/upload/`,
177
+ filePart(file),
178
+ signalOf(options)
179
+ ),
180
+
181
+ uploadVideo: (file, options) =>
182
+ client.post("/upload/video/", filePart(file), signalOf(options)),
183
+
184
+ uploadFile: (file, options) =>
185
+ client.post("/upload/file/", filePart(file), signalOf(options)),
186
+ };
187
+ }