@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.
- package/CHANGELOG.md +148 -0
- package/LICENSE +166 -0
- package/README.md +140 -0
- package/api/public-api.md +1070 -0
- package/dist/adapter-C_0mhENy.d.ts +320 -0
- package/dist/admin/admin.css +5 -0
- package/dist/admin/index.d.ts +265 -0
- package/dist/admin/index.js +393 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/tokens.css +597 -0
- package/dist/admin/ui.d.ts +3013 -0
- package/dist/admin/ui.js +9783 -0
- package/dist/admin/ui.js.map +1 -0
- package/dist/auth/index.d.ts +4 -0
- package/dist/auth/index.js +9 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth-B2gBtHl5.d.ts +28 -0
- package/dist/can-Chy7zmx7.d.ts +69 -0
- package/dist/chunk-3I6337WJ.js +83 -0
- package/dist/chunk-3I6337WJ.js.map +1 -0
- package/dist/chunk-4Z6P3XCE.js +62 -0
- package/dist/chunk-4Z6P3XCE.js.map +1 -0
- package/dist/chunk-6E3ZS5Z5.js +1007 -0
- package/dist/chunk-6E3ZS5Z5.js.map +1 -0
- package/dist/chunk-77JDRXXU.js +3 -0
- package/dist/chunk-77JDRXXU.js.map +1 -0
- package/dist/chunk-FHYWJCG6.js +171 -0
- package/dist/chunk-FHYWJCG6.js.map +1 -0
- package/dist/chunk-JAE7HHMM.js +506 -0
- package/dist/chunk-JAE7HHMM.js.map +1 -0
- package/dist/chunk-NPDBQBLW.js +68 -0
- package/dist/chunk-NPDBQBLW.js.map +1 -0
- package/dist/chunk-ORMYARXD.js +6 -0
- package/dist/chunk-ORMYARXD.js.map +1 -0
- package/dist/chunk-R4O7APIX.js +51 -0
- package/dist/chunk-R4O7APIX.js.map +1 -0
- package/dist/chunk-ROLKRJWI.js +187 -0
- package/dist/chunk-ROLKRJWI.js.map +1 -0
- package/dist/chunk-SOTQ3WP5.js +31 -0
- package/dist/chunk-SOTQ3WP5.js.map +1 -0
- package/dist/chunk-TCFFHTY3.js +97 -0
- package/dist/chunk-TCFFHTY3.js.map +1 -0
- package/dist/chunk-TS7Y7GEZ.js +7 -0
- package/dist/chunk-TS7Y7GEZ.js.map +1 -0
- package/dist/chunk-VLGR5PZX.js +3 -0
- package/dist/chunk-VLGR5PZX.js.map +1 -0
- package/dist/chunk-ZW2PUSVS.js +40 -0
- package/dist/chunk-ZW2PUSVS.js.map +1 -0
- package/dist/chunk-ZYNBMYJ6.js +392 -0
- package/dist/chunk-ZYNBMYJ6.js.map +1 -0
- package/dist/cli/nonext-husk.js +3360 -0
- package/dist/cli/nonext-husk.js.map +1 -0
- package/dist/cli/templates/firestore.rules +393 -0
- package/dist/cli/templates/storage.rules +165 -0
- package/dist/client/index.d.ts +27 -0
- package/dist/client/index.js +15 -0
- package/dist/client/index.js.map +1 -0
- package/dist/cms-9fSA3uQV.d.ts +158 -0
- package/dist/core/index.d.ts +1267 -0
- package/dist/core/index.js +12 -0
- package/dist/core/index.js.map +1 -0
- package/dist/entry-D3kiE8T1.d.ts +163 -0
- package/dist/field-Diyf5siF.d.ts +158 -0
- package/dist/field-type-D25Vojb0.d.ts +157 -0
- package/dist/firebase/index.d.ts +251 -0
- package/dist/firebase/index.js +991 -0
- package/dist/firebase/index.js.map +1 -0
- package/dist/index-5ylaMBUH.d.ts +155 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/permissions/index.d.ts +101 -0
- package/dist/permissions/index.js +7 -0
- package/dist/permissions/index.js.map +1 -0
- package/dist/registry-CUC25qay.d.ts +59 -0
- package/dist/richtext/index.d.ts +85 -0
- package/dist/richtext/index.js +127 -0
- package/dist/richtext/index.js.map +1 -0
- package/dist/schema/index.d.ts +50 -0
- package/dist/schema/index.js +5 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/server/index.d.ts +39 -0
- package/dist/server/index.js +25 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types/index.d.ts +36 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/write-limits-Djmm6L3R.d.ts +215 -0
- 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 };
|