@nonext/husk 0.2.3

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 (89) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/LICENSE +166 -0
  3. package/README.md +140 -0
  4. package/api/public-api.md +1070 -0
  5. package/dist/adapter-C_0mhENy.d.ts +320 -0
  6. package/dist/admin/admin.css +5 -0
  7. package/dist/admin/index.d.ts +265 -0
  8. package/dist/admin/index.js +393 -0
  9. package/dist/admin/index.js.map +1 -0
  10. package/dist/admin/tokens.css +597 -0
  11. package/dist/admin/ui.d.ts +3013 -0
  12. package/dist/admin/ui.js +9783 -0
  13. package/dist/admin/ui.js.map +1 -0
  14. package/dist/auth/index.d.ts +4 -0
  15. package/dist/auth/index.js +9 -0
  16. package/dist/auth/index.js.map +1 -0
  17. package/dist/auth-B2gBtHl5.d.ts +28 -0
  18. package/dist/can-Chy7zmx7.d.ts +69 -0
  19. package/dist/chunk-3I6337WJ.js +83 -0
  20. package/dist/chunk-3I6337WJ.js.map +1 -0
  21. package/dist/chunk-4Z6P3XCE.js +62 -0
  22. package/dist/chunk-4Z6P3XCE.js.map +1 -0
  23. package/dist/chunk-6E3ZS5Z5.js +1007 -0
  24. package/dist/chunk-6E3ZS5Z5.js.map +1 -0
  25. package/dist/chunk-77JDRXXU.js +3 -0
  26. package/dist/chunk-77JDRXXU.js.map +1 -0
  27. package/dist/chunk-FHYWJCG6.js +171 -0
  28. package/dist/chunk-FHYWJCG6.js.map +1 -0
  29. package/dist/chunk-JAE7HHMM.js +506 -0
  30. package/dist/chunk-JAE7HHMM.js.map +1 -0
  31. package/dist/chunk-NPDBQBLW.js +68 -0
  32. package/dist/chunk-NPDBQBLW.js.map +1 -0
  33. package/dist/chunk-ORMYARXD.js +6 -0
  34. package/dist/chunk-ORMYARXD.js.map +1 -0
  35. package/dist/chunk-R4O7APIX.js +51 -0
  36. package/dist/chunk-R4O7APIX.js.map +1 -0
  37. package/dist/chunk-ROLKRJWI.js +187 -0
  38. package/dist/chunk-ROLKRJWI.js.map +1 -0
  39. package/dist/chunk-SOTQ3WP5.js +31 -0
  40. package/dist/chunk-SOTQ3WP5.js.map +1 -0
  41. package/dist/chunk-TCFFHTY3.js +97 -0
  42. package/dist/chunk-TCFFHTY3.js.map +1 -0
  43. package/dist/chunk-TS7Y7GEZ.js +7 -0
  44. package/dist/chunk-TS7Y7GEZ.js.map +1 -0
  45. package/dist/chunk-VLGR5PZX.js +3 -0
  46. package/dist/chunk-VLGR5PZX.js.map +1 -0
  47. package/dist/chunk-ZW2PUSVS.js +40 -0
  48. package/dist/chunk-ZW2PUSVS.js.map +1 -0
  49. package/dist/chunk-ZYNBMYJ6.js +392 -0
  50. package/dist/chunk-ZYNBMYJ6.js.map +1 -0
  51. package/dist/cli/nonext-husk.js +3360 -0
  52. package/dist/cli/nonext-husk.js.map +1 -0
  53. package/dist/cli/templates/firestore.rules +393 -0
  54. package/dist/cli/templates/storage.rules +165 -0
  55. package/dist/client/index.d.ts +27 -0
  56. package/dist/client/index.js +15 -0
  57. package/dist/client/index.js.map +1 -0
  58. package/dist/cms-9fSA3uQV.d.ts +158 -0
  59. package/dist/core/index.d.ts +1267 -0
  60. package/dist/core/index.js +12 -0
  61. package/dist/core/index.js.map +1 -0
  62. package/dist/entry-D3kiE8T1.d.ts +163 -0
  63. package/dist/field-Diyf5siF.d.ts +158 -0
  64. package/dist/field-type-D25Vojb0.d.ts +157 -0
  65. package/dist/firebase/index.d.ts +251 -0
  66. package/dist/firebase/index.js +991 -0
  67. package/dist/firebase/index.js.map +1 -0
  68. package/dist/index-5ylaMBUH.d.ts +155 -0
  69. package/dist/index.d.ts +14 -0
  70. package/dist/index.js +17 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/permissions/index.d.ts +101 -0
  73. package/dist/permissions/index.js +7 -0
  74. package/dist/permissions/index.js.map +1 -0
  75. package/dist/registry-CUC25qay.d.ts +59 -0
  76. package/dist/richtext/index.d.ts +85 -0
  77. package/dist/richtext/index.js +127 -0
  78. package/dist/richtext/index.js.map +1 -0
  79. package/dist/schema/index.d.ts +50 -0
  80. package/dist/schema/index.js +5 -0
  81. package/dist/schema/index.js.map +1 -0
  82. package/dist/server/index.d.ts +39 -0
  83. package/dist/server/index.js +25 -0
  84. package/dist/server/index.js.map +1 -0
  85. package/dist/types/index.d.ts +36 -0
  86. package/dist/types/index.js +6 -0
  87. package/dist/types/index.js.map +1 -0
  88. package/dist/write-limits-Djmm6L3R.d.ts +215 -0
  89. package/package.json +136 -0
@@ -0,0 +1,251 @@
1
+ import { Firestore } from 'firebase/firestore';
2
+ import { C as CmsAdapter, g as MediaAdapter } from '../adapter-C_0mhENy.js';
3
+ import { R as Registry } from '../registry-CUC25qay.js';
4
+ import { r as WriteRateLimiter, l as MediaRejection } from '../write-limits-Djmm6L3R.js';
5
+ import { f as FieldTypeRegistry } from '../field-type-D25Vojb0.js';
6
+ import { b as AuthUser } from '../auth-B2gBtHl5.js';
7
+ export { A as AuthError, a as AuthErrorCode, C as ClaimSet, b as ClaimSync, c as ClaimSyncError, E as EmulatorClaimSyncOptions, H as HuskAuth, d as HuskAuthDeps, N as NewUser, S as SignUpInput, e as StoredUser, U as UserPatch, f as createEmulatorClaimSync, g as createHuskAuth, h as createUnavailableClaimSync, p as patchUser, r as readUser, t as toAuthUser, w as writeUser } from '../index-5ylaMBUH.js';
8
+ import { FirebaseStorage } from 'firebase/storage';
9
+ import { M as MediaItem } from '../field-Diyf5siF.js';
10
+ import '../entry-D3kiE8T1.js';
11
+ import 'react';
12
+ import 'firebase/auth';
13
+
14
+ interface FirestoreAdapterDeps {
15
+ db: Firestore;
16
+ /**
17
+ * The live registry. Supplied by the CMS instance, which owns it: the adapter must not
18
+ * build one, because the registry is assembled from code-defined types the adapter cannot
19
+ * see plus stored ones it reads through `schemas`.
20
+ *
21
+ * A function rather than a value, because the database half changes while the admin runs
22
+ * and every call has to see the current merge. It is also what makes the lock guard
23
+ * structural: an adapter cannot be constructed without the registry that says which types
24
+ * are code-defined and which are locked, so `schemas.update` and `schemas.delete` always
25
+ * have the answer available and never have to trust the caller for it.
26
+ */
27
+ registry: () => Registry;
28
+ currentUser: () => AuthUser;
29
+ /**
30
+ * The session, for read-side permission checks. Optional, and its absence is meaningful.
31
+ *
32
+ * `currentUser` is the write path's account and throws on a connection that has none,
33
+ * which is what proves a public page never asks who is reading. So the read check gets
34
+ * its own accessor: the admin passes one and its `list`, `get` and `count` are refused
35
+ * for a content type this account may not read, while a customer's website omits it and
36
+ * the read path stays free of any question about a session. The security rules refuse a
37
+ * denied read either way; this is the layer that refuses it before the round trip.
38
+ */
39
+ session?: () => AuthUser | null;
40
+ /** Field type specs. Optional until M5; the converters fall back to defaults without it. */
41
+ fieldTypes?: FieldTypeRegistry;
42
+ /**
43
+ * The burst guard for this adapter, or `null` for a process that is legitimately a bulk
44
+ * writer: the seed loader, a migration run, a one-off import.
45
+ *
46
+ * Passing `null` is a statement, not a convenience. It says "the thing the burst guard
47
+ * exists to catch is exactly what this process is doing", and it gives up nothing the
48
+ * security rules enforce, because the rules-side floor is per document and this guard is
49
+ * per process. See core/write-limits.ts.
50
+ */
51
+ rateLimiter?: WriteRateLimiter | null;
52
+ }
53
+ declare function createFirestoreAdapter(deps: FirestoreAdapterDeps): CmsAdapter;
54
+
55
+ /**
56
+ * How the media library starts an upload, without knowing that Storage exists.
57
+ *
58
+ * The same seam as `RelationSource` for the relation picker, and for the same reason: the
59
+ * screen lives under `src/admin/**`, where the Firebase SDK may not be imported, and it
60
+ * still has to offer progress, cancel and retry. The host builds one with
61
+ * `createMediaUploader` from `@nonext/husk/firebase` and hands it in.
62
+ *
63
+ * Every member below mirrors what `createMediaUpload` already returns, so the real
64
+ * implementation is a pass-through and a test implementation is a few lines.
65
+ */
66
+ type MediaUploadPhase = "idle" | "uploading" | "storing" | "done" | "cancelled" | "failed";
67
+ interface MediaUploadProgress {
68
+ phase: MediaUploadPhase;
69
+ bytesTransferred: number;
70
+ totalBytes: number;
71
+ /** 0 to 1. Exactly 1 only once every byte is acknowledged. */
72
+ fraction: number;
73
+ /** 1 for the first run, 2 for the first retry, and so on. */
74
+ attempt: number;
75
+ }
76
+ interface MediaUploadHandle {
77
+ readonly mediaId: string;
78
+ readonly path: string;
79
+ readonly fileName: string;
80
+ progress(): MediaUploadProgress;
81
+ start(): Promise<MediaItem>;
82
+ /** Runs it again to the same id and the same path, so remains are overwritten. */
83
+ retry(): Promise<MediaItem>;
84
+ cancel(): void;
85
+ }
86
+ interface MediaUploadRequest {
87
+ onProgress?: (progress: MediaUploadProgress) => void;
88
+ alt?: string | null;
89
+ }
90
+ interface MediaUploader {
91
+ create(file: File, options?: MediaUploadRequest): MediaUploadHandle;
92
+ /**
93
+ * New bytes for an item that already exists, keeping its id and its object path.
94
+ *
95
+ * The MIME type may not change: the path is fixed for the life of the item and the rules
96
+ * tie the path's extension to the declared type. The implementation refuses a mismatch
97
+ * with a sentence rather than letting it surface as a permission error.
98
+ */
99
+ replace(existing: MediaItem, file: File, options?: MediaUploadRequest): MediaUploadHandle;
100
+ }
101
+
102
+ /**
103
+ * Uploading one file into the media library.
104
+ *
105
+ * An upload is two writes that cannot be made atomic: the bytes go to Storage, the metadata
106
+ * document goes to Firestore, and no transaction spans the two. The order is therefore a
107
+ * decision rather than an accident, and it is this one: **bytes first, document last**.
108
+ *
109
+ * - A document written before the bytes would point at an object that does not exist. The
110
+ * library would list it, the picker would offer it, an entry could reference it, and a
111
+ * visitor would get a broken image on the customer's site. The failure would be visible
112
+ * everywhere except where it happened.
113
+ * - An object written before the document is invisible to the CMS, because the CMS lists
114
+ * documents, not bucket contents. It costs storage and nothing else, and M7's orphan
115
+ * sweep is defined precisely as "an object with no document".
116
+ *
117
+ * So when the document write fails after the bytes landed, this module tries to delete the
118
+ * object it just wrote (see `removeOrphan`). If that cleanup also fails, the upload does not
119
+ * pretend to have been clean: the thrown `MediaUploadError` carries `orphanedPath`, naming
120
+ * the object still sitting in the bucket, so the admin can say so and the sweep has a
121
+ * starting point. Nothing is swallowed and nothing is guessed.
122
+ *
123
+ * What this module still cannot cover: a browser closed between the two writes. That leaves
124
+ * an orphan no client will ever clean up, which is the reason the sweep exists rather than
125
+ * being a nice-to-have.
126
+ */
127
+ type UploadPhase = "idle" | "uploading" | "storing" | "done" | "cancelled" | "failed";
128
+ interface UploadProgress {
129
+ phase: UploadPhase;
130
+ bytesTransferred: number;
131
+ totalBytes: number;
132
+ /** 0 to 1. Exactly 1 only once every byte is acknowledged by Storage. */
133
+ fraction: number;
134
+ /** 1 for the first run, 2 for the first retry, and so on. */
135
+ attempt: number;
136
+ }
137
+ interface ImageSize {
138
+ width: number;
139
+ height: number;
140
+ }
141
+ /**
142
+ * Reads the pixel size of an image, or returns null when it cannot.
143
+ *
144
+ * Injected rather than imported so the upload path stays testable without a DOM, and so a
145
+ * host that has a better decoder than the browser can supply one.
146
+ */
147
+ type ImageSizeReader = (file: Blob, type: string) => Promise<ImageSize | null>;
148
+ interface MediaUploadDeps {
149
+ storage: FirebaseStorage;
150
+ media: MediaAdapter;
151
+ /** Whose upload this is. Written into the object's custom metadata; see storage.rules. */
152
+ currentUser: () => AuthUser;
153
+ /** Defaults to `browserImageSize`. */
154
+ readImageSize?: ImageSizeReader;
155
+ }
156
+ interface MediaUploadOptions {
157
+ onProgress?: (progress: UploadProgress) => void;
158
+ /** Alt text captured in the upload form, if any. Editable afterwards. */
159
+ alt?: string | null;
160
+ }
161
+ type MediaUploadErrorCode = "rejected" | "cancelled" | "storage-failed" | "metadata-failed";
162
+ declare class MediaUploadError extends Error {
163
+ readonly name = "MediaUploadError";
164
+ readonly code: MediaUploadErrorCode;
165
+ /** Set when the file itself was refused, so the UI can point at the offending rule. */
166
+ readonly rejection: MediaRejection | null;
167
+ /**
168
+ * The Storage object that outlived the failed upload, when cleanup could not remove it.
169
+ * Null means nothing was left behind, which is the ordinary case.
170
+ */
171
+ readonly orphanedPath: string | null;
172
+ constructor(code: MediaUploadErrorCode, message: string, detail?: {
173
+ rejection?: MediaRejection;
174
+ orphanedPath?: string;
175
+ cause?: unknown;
176
+ });
177
+ }
178
+ interface MediaUpload {
179
+ /** Allocated before anything is written, because the object path contains it. */
180
+ readonly mediaId: string;
181
+ /** The Storage object path this upload writes to, on every attempt. */
182
+ readonly path: string;
183
+ readonly fileName: string;
184
+ progress(): UploadProgress;
185
+ /** Runs the upload. Rejects with `MediaUploadError`; resolves with the stored item. */
186
+ start(): Promise<MediaItem>;
187
+ /**
188
+ * Runs it again after a failure or a cancellation, to the same id and the same path, so
189
+ * a retried upload overwrites its own remains instead of accumulating them.
190
+ */
191
+ retry(): Promise<MediaItem>;
192
+ /** Aborts an attempt in flight. A no-op before the first attempt and after success. */
193
+ cancel(): void;
194
+ }
195
+ /** Prepares an upload without starting it, which is what gives the caller a cancel handle. */
196
+ declare function createMediaUpload(deps: MediaUploadDeps, file: File, options?: MediaUploadOptions): MediaUpload;
197
+ /**
198
+ * Replacing the bytes of an item that is already in the library.
199
+ *
200
+ * Replace keeps the media id and the Storage object path, which is what makes it a replace
201
+ * rather than a delete followed by an upload: every entry that references the item goes on
202
+ * referencing the same item, and no reference has to be repaired by hand.
203
+ *
204
+ * Keeping the path has one consequence that is a rule rather than an accident: the
205
+ * replacement must be of the same MIME type as the original. The object path ends in the
206
+ * file name, `storage.rules` requires the name's extension to match the declared content
207
+ * type, and `firestore.rules` forbids a media document's `path` from ever changing. A JPEG
208
+ * cannot therefore take the place of a PNG. That is refused here, with a sentence, rather
209
+ * than left to surface as a permission error.
210
+ *
211
+ * What the caller must do afterwards, and what this function deliberately does not do:
212
+ * overwriting an object mints a new download URL, so the `url` denormalized into every
213
+ * referencing entry is now stale. `media.refreshReferences(mediaId, url)` rewrites them.
214
+ * It is a separate call because it is a batch of entry writes with its own partial-failure
215
+ * story, and burying that inside an upload handle would hide it.
216
+ *
217
+ * The one state it cannot make safe: if the bytes land and the document update fails, the
218
+ * object holds the new file while the record still describes the old one, and the old url
219
+ * is already dead. There is nothing to compensate with, because deleting the object would
220
+ * destroy the item outright. The error says so plainly and asks for another attempt.
221
+ */
222
+ declare function createMediaReplace(deps: MediaUploadDeps, existing: MediaItem, file: File, options?: MediaUploadOptions): MediaUpload;
223
+ /** Prepares and starts an upload in one call, for callers with nothing to cancel. */
224
+ declare function uploadMedia(deps: MediaUploadDeps, file: File, options?: MediaUploadOptions): Promise<MediaItem>;
225
+ /**
226
+ * Deletes an object left behind by a half-finished upload. Reports whether it is gone.
227
+ *
228
+ * True means the bucket confirmed it: the delete succeeded, or the object was already
229
+ * reported missing. Anything else, a permission denial above all, is false, so the caller
230
+ * names the path instead of claiming a cleanup that did not happen.
231
+ *
232
+ * One asymmetry worth knowing, because it looks like a bug from the outside: the storage
233
+ * rules let an author delete only an object whose custom metadata names them, and a path
234
+ * with no object has no metadata to name anyone. An author asking to remove something that
235
+ * was never there is therefore denied rather than told it is missing, and gets false. That
236
+ * is the safe direction: the only caller is the compensating delete after a failed metadata
237
+ * write, where the object demonstrably exists and belongs to the caller.
238
+ */
239
+ declare function removeOrphan(storage: FirebaseStorage, path: string): Promise<boolean>;
240
+ /** The default reader. Returns null wherever `createImageBitmap` is unavailable. */
241
+ declare const browserImageSize: ImageSizeReader;
242
+ /**
243
+ * The upload seam the admin is written against.
244
+ *
245
+ * The media library screen starts, cancels and retries uploads, and it lives under
246
+ * `src/admin/**`, where Firebase may not be imported. It therefore takes a `MediaUploader`
247
+ * and the host builds one here, exactly as the relation picker takes a `RelationSource`.
248
+ */
249
+ declare function createMediaUploader(deps: MediaUploadDeps): MediaUploader;
250
+
251
+ export { type FirestoreAdapterDeps, type ImageSize, type ImageSizeReader, type MediaUpload, type MediaUploadDeps, MediaUploadError, type MediaUploadErrorCode, type MediaUploadOptions, type UploadPhase, type UploadProgress, browserImageSize, createFirestoreAdapter, createMediaReplace, createMediaUpload, createMediaUploader, removeOrphan, uploadMedia };