@dereekb/firebase 13.42.0 → 14.0.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 (33) hide show
  1. package/eslint/index.esm.js +209 -302
  2. package/eslint/package.json +9 -9
  3. package/index.esm.js +5774 -3926
  4. package/package.json +10 -10
  5. package/src/lib/common/firestore/accessor/document.rxjs.d.ts +0 -4
  6. package/src/lib/common/firestore/query/accumulator.d.ts +2 -2
  7. package/src/lib/common/firestore/query/iterator.d.ts +1 -1
  8. package/src/lib/common/model/function.d.ts +0 -14
  9. package/src/lib/model/formspace/formspace.access.d.ts +139 -0
  10. package/src/lib/model/formspace/formspace.action.d.ts +34 -0
  11. package/src/lib/model/formspace/formspace.api.d.ts +240 -0
  12. package/src/lib/model/formspace/formspace.api.error.d.ts +96 -0
  13. package/src/lib/model/formspace/formspace.d.ts +461 -0
  14. package/src/lib/model/formspace/formspace.id.d.ts +60 -0
  15. package/src/lib/model/formspace/formspace.permission.d.ts +47 -0
  16. package/src/lib/model/formspace/formspace.processing.d.ts +86 -0
  17. package/src/lib/model/formspace/formspace.query.d.ts +110 -0
  18. package/src/lib/model/formspace/formspace.task.d.ts +136 -0
  19. package/src/lib/model/formspace/formspace.type.d.ts +300 -0
  20. package/src/lib/model/formspace/formspace.upload.d.ts +204 -0
  21. package/src/lib/model/formspace/formspace.util.d.ts +514 -0
  22. package/src/lib/model/formspace/index.d.ts +13 -0
  23. package/src/lib/model/index.d.ts +1 -0
  24. package/src/lib/model/notification/notification.task.d.ts +3 -1
  25. package/src/lib/model/storagefile/storagefile.api.d.ts +19 -0
  26. package/src/lib/model/storagefile/storagefile.file.d.ts +42 -2
  27. package/src/lib/model/storagefile/storagefile.upload.d.ts +30 -0
  28. package/test/index.esm.js +158 -258
  29. package/test/package.json +8 -8
  30. package/test/src/lib/common/firebase.instance.d.ts +0 -4
  31. package/test/src/lib/common/firestore/firestore.instance.d.ts +0 -4
  32. package/test/src/lib/common/mock/mock.item.collection.fixture.d.ts +0 -7
  33. package/test/src/lib/common/storage/storage.instance.d.ts +0 -4
@@ -0,0 +1,110 @@
1
+ import { type Maybe } from '@dereekb/util';
2
+ import { type FirestoreQueryConstraint } from '../../common/firestore/query/constraint';
3
+ import { type FirebaseAuthOwnershipKey } from '../../common/auth/auth';
4
+ import { type FormSpaceKey } from './formspace.id';
5
+ /**
6
+ * @module formspace.query
7
+ *
8
+ * SINGLE-FIELD BY CONSTRUCTION. `firestore.indexes.json` is generated from downstream `-firebase`
9
+ * components and cannot resolve an identity declared upstream here, so a FormSpace query that needed a
10
+ * composite index would have no way to declare one. Every query below is therefore one field plus an
11
+ * optional sort on that same field, which Firestore serves from its automatic single-field indexes.
12
+ */
13
+ /**
14
+ * Returns query constraints for FormSpaces awaiting a processing task (`ps == QUEUED_FOR_PROCESSING`).
15
+ *
16
+ * This is the backstop sweep: submission normally creates the task inline, so a space that is still sitting
17
+ * here is one whose task creation was lost.
18
+ *
19
+ * @param limitCount - Maximum number of results. Omit for unbounded.
20
+ * @returns Firestore query constraints for FormSpaces queued for processing.
21
+ *
22
+ * @dbxModelFirebaseIndex
23
+ * @dbxModelFirebaseIndexModel FormSpace
24
+ * @dbxModelFirebaseIndexScope COLLECTION
25
+ * @dbxModelFirebaseIndexCategory sweep
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const constraints = formSpacesQueuedForProcessingQuery(100);
30
+ * ```
31
+ */
32
+ export declare function formSpacesQueuedForProcessingQuery(limitCount?: Maybe<number>): FirestoreQueryConstraint[];
33
+ /**
34
+ * Input for {@link formSpacesDueForExpirationQuery}.
35
+ */
36
+ export interface FormSpacesDueForExpirationQueryInput {
37
+ /**
38
+ * FormSpaces whose `eat` is at or before this instant are due.
39
+ *
40
+ * PIN THIS for the whole sweep rather than re-deriving it per page — a cutoff that advances with the
41
+ * clock lets a space that ages mid-sweep join a page not yet reached, making the pass unbounded.
42
+ */
43
+ readonly before: Date;
44
+ /**
45
+ * Maximum number of results per page.
46
+ */
47
+ readonly limit?: Maybe<number>;
48
+ }
49
+ /**
50
+ * Returns query constraints for FormSpaces whose expiration instant has arrived.
51
+ *
52
+ * NOTE: a Firestore inequality skips documents where the field is absent, so a space with no `eat` — one
53
+ * whose type does not expire, or one that has already been submitted or expired and had `eat` cleared — is
54
+ * not matched. That absence IS the exclusion mechanism; there is no second flag to keep in step.
55
+ *
56
+ * @param input - The pinned cutoff and page size.
57
+ * @returns Firestore query constraints for FormSpaces due for expiration.
58
+ *
59
+ * @dbxModelFirebaseIndex
60
+ * @dbxModelFirebaseIndexModel FormSpace
61
+ * @dbxModelFirebaseIndexScope COLLECTION
62
+ * @dbxModelFirebaseIndexCategory cleanup
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * const constraints = formSpacesDueForExpirationQuery({ before: new Date(), limit: 50 });
67
+ * ```
68
+ */
69
+ export declare function formSpacesDueForExpirationQuery(input: FormSpacesDueForExpirationQueryInput): FirestoreQueryConstraint[];
70
+ /**
71
+ * Returns query constraints for every StorageFile that belongs to a FormSpace.
72
+ *
73
+ * A single `array-contains` on the group id, which Firestore serves from its automatic array index — the
74
+ * FormSpace's own group id is the only handle needed, so nothing here requires a composite index.
75
+ *
76
+ * @param formSpaceKey - The FormSpace whose files to select.
77
+ * @returns Firestore query constraints for the space's StorageFiles.
78
+ *
79
+ * @dbxModelFirebaseIndex
80
+ * @dbxModelFirebaseIndexModel StorageFile
81
+ * @dbxModelFirebaseIndexScope COLLECTION
82
+ * @dbxModelFirebaseIndexCategory lookup
83
+ * @dbxModelFirebaseIndexSkip true
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * const constraints = storageFilesForFormSpaceQuery('fsp/abc123');
88
+ * ```
89
+ */
90
+ export declare function storageFilesForFormSpaceQuery(formSpaceKey: FormSpaceKey): FirestoreQueryConstraint[];
91
+ /**
92
+ * Returns query constraints for every FormSpace carrying the given ownership key.
93
+ *
94
+ * Ordering is deliberately left to the caller/client: a second field here would need a composite index this
95
+ * package has no way to declare.
96
+ *
97
+ * @param ownerKey - The ownership key to filter by.
98
+ * @returns Firestore query constraints for the owner's FormSpaces.
99
+ *
100
+ * @dbxModelFirebaseIndex
101
+ * @dbxModelFirebaseIndexModel FormSpace
102
+ * @dbxModelFirebaseIndexScope COLLECTION
103
+ * @dbxModelFirebaseIndexCategory lookup
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * const constraints = formSpacesForOwnerQuery('pr/abc123');
108
+ * ```
109
+ */
110
+ export declare function formSpacesForOwnerQuery(ownerKey: FirebaseAuthOwnershipKey): FirestoreQueryConstraint[];
@@ -0,0 +1,136 @@
1
+ import { type Maybe } from '@dereekb/util';
2
+ import { type CreateNotificationTaskTemplate } from '../notification/notification.create.task';
3
+ import { type NotificationTaskSubtaskCheckpointString, type NotificationTaskSubtaskData, type NotificationTaskSubtaskMetadata } from '../notification/notification.task.subtask';
4
+ import { type NotificationTaskType, type NotificationTaskUniqueId } from '../notification/notification.id';
5
+ import { type FormSpaceDocument } from './formspace';
6
+ import { type FormSpaceId, type FormSpaceType } from './formspace.id';
7
+ /**
8
+ * @module formspace.task
9
+ *
10
+ * The NotificationTask that processes a submitted FormSpace.
11
+ *
12
+ * ONE task type for every form type. The task's SUBTASK TARGET is the {@link FormSpaceType}, so a new form
13
+ * type registers a processor rather than a task type — the same specialization
14
+ * `StorageFileProcessingNotificationTask` already does by purpose.
15
+ *
16
+ * ONE task per SUBMISSION ATTEMPT, not per space. A unique task's document id is derived and permanent, and
17
+ * a completed task is only marked done (`d`) — it lingers until the cleanup sweep collects it. So a space
18
+ * that is reopened and resubmitted would re-derive the id of its own finished task, and
19
+ * `createOrRunUniqueNotificationDocument` would find it already there and do nothing at all: the space
20
+ * would sit in QUEUED_FOR_PROCESSING pointing at a dead document forever. Keying the id by the space's
21
+ * reopen count is what keeps each attempt a document of its own, and the first attempt keeps the exact id
22
+ * it has always had.
23
+ */
24
+ /**
25
+ * NotificationTask type identifier for FormSpace submission processing.
26
+ */
27
+ export declare const FORM_SPACE_SUBMISSION_NOTIFICATION_TASK_TYPE: NotificationTaskType;
28
+ /**
29
+ * Checkpoint string for a FormSpace submission subtask.
30
+ */
31
+ export type FormSpaceSubmissionSubtask = NotificationTaskSubtaskCheckpointString;
32
+ /**
33
+ * Arbitrary metadata carried between a FormSpace submission's subtasks.
34
+ */
35
+ export type FormSpaceSubmissionSubtaskMetadata = NotificationTaskSubtaskMetadata;
36
+ /**
37
+ * Data payload for a FormSpace submission NotificationTask.
38
+ *
39
+ * @template M - subtask metadata type
40
+ * @template S - subtask checkpoint string type
41
+ */
42
+ export interface FormSpaceSubmissionNotificationTaskData<M extends FormSpaceSubmissionSubtaskMetadata = FormSpaceSubmissionSubtaskMetadata, S extends FormSpaceSubmissionSubtask = FormSpaceSubmissionSubtask> extends NotificationTaskSubtaskData<M, S> {
43
+ /**
44
+ * The FormSpaceDocument id.
45
+ */
46
+ readonly formSpace: FormSpaceId;
47
+ /**
48
+ * The FormSpace's type, which is also the subtask target.
49
+ *
50
+ * Retrieved from the FormSpace the first time the task runs and re-copied onto the metadata afterwards,
51
+ * so subsequent runs do not re-read the document only to learn which processor to dispatch to.
52
+ */
53
+ readonly t?: Maybe<FormSpaceType>;
54
+ /**
55
+ * The space's reopen count when this task was created — the submission ATTEMPT this task belongs to.
56
+ *
57
+ * The handler compares it against the space's current count and terminates when they differ. Without
58
+ * that fence a task left over from a superseded attempt would process the reopened space's new content
59
+ * and its cleanup would write `ps`/`cpat`/`pn` over the attempt actually in force. Absent on a task
60
+ * created before attempt-keying existed, which is read as "do not fence".
61
+ */
62
+ readonly rc?: Maybe<number>;
63
+ }
64
+ /**
65
+ * Input for {@link formSpaceSubmissionNotificationTaskTemplate}.
66
+ */
67
+ export interface FormSpaceSubmissionNotificationTaskInput<M extends FormSpaceSubmissionSubtaskMetadata = FormSpaceSubmissionSubtaskMetadata> extends Omit<FormSpaceSubmissionNotificationTaskData<M>, 'formSpace' | 't' | 'sfps' | 'rc'> {
68
+ readonly formSpaceDocument: FormSpaceDocument;
69
+ /**
70
+ * The space's current reopen count — the attempt this task is for. Defaults to 0, the first submission.
71
+ */
72
+ readonly reopenCount?: Maybe<number>;
73
+ readonly overrideExistingTask?: Maybe<boolean>;
74
+ }
75
+ /**
76
+ * Input for {@link formSpaceSubmissionNotificationTaskUniqueId}.
77
+ */
78
+ export interface FormSpaceSubmissionNotificationTaskUniqueIdInput {
79
+ readonly formSpaceId: FormSpaceId;
80
+ /**
81
+ * The attempt's reopen count. Defaults to 0, the first submission.
82
+ */
83
+ readonly reopenCount?: Maybe<number>;
84
+ }
85
+ /**
86
+ * Returns the unique NotificationTask document id processing one SUBMISSION ATTEMPT of a FormSpace.
87
+ *
88
+ * The first attempt is the bare {@link notificationTaskUniqueId}, unsuffixed, so a space submitted before
89
+ * reopening existed keeps the exact document it already has. Every attempt after a reopen appends its
90
+ * count, which is what stops a resubmit from colliding with the finished task of the attempt before it.
91
+ *
92
+ * The `r` between the separators keeps the suffix from ever being able to produce a `__` run, whatever a
93
+ * FormSpace id happens to contain — Firestore reserves ids wrapped in double underscores, and staying well
94
+ * clear of them costs one character.
95
+ *
96
+ * Exported so a caller that has to LOCATE an attempt's task — a test asserting which document a resubmit
97
+ * created, or tooling inspecting a stuck submission — derives the id the same way the template does. The
98
+ * reopen action deliberately does not need it: the superseded attempt's task is left alone, either already
99
+ * done and awaiting the cleanup sweep or still queued and about to fence itself off.
100
+ *
101
+ * @param input - The FormSpace id and the attempt's reopen count.
102
+ * @returns The unique notification task id.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * formSpaceSubmissionNotificationTaskUniqueId({ formSpaceId: 'abc' }); // 'abc_FSPS'
107
+ * formSpaceSubmissionNotificationTaskUniqueId({ formSpaceId: 'abc', reopenCount: 2 }); // 'abc_FSPS_r2'
108
+ * ```
109
+ *
110
+ * @__NO_SIDE_EFFECTS__
111
+ */
112
+ export declare function formSpaceSubmissionNotificationTaskUniqueId(input: FormSpaceSubmissionNotificationTaskUniqueIdInput): NotificationTaskUniqueId;
113
+ /**
114
+ * Creates a {@link CreateNotificationTaskTemplate} for a FormSpace submission task.
115
+ *
116
+ * The task is UNIQUE to one submission ATTEMPT of the FormSpace. Within an attempt that uniqueness is what
117
+ * makes a second template resolve to the same document rather than racing a second processor against the
118
+ * first; across attempts, {@link formSpaceSubmissionNotificationTaskUniqueId} keys them apart so a resubmit
119
+ * after a reopen gets a document — and a checkpoint flow — of its own.
120
+ *
121
+ * @param input - The target FormSpaceDocument, its reopen count, and optional subtask data.
122
+ * @returns A CreateNotificationTaskTemplate for the submission task.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * const template = formSpaceSubmissionNotificationTaskTemplate({ formSpaceDocument: doc, reopenCount: formSpace.rc });
127
+ * ```
128
+ */
129
+ export declare function formSpaceSubmissionNotificationTaskTemplate(input: FormSpaceSubmissionNotificationTaskInput): CreateNotificationTaskTemplate;
130
+ /**
131
+ * All NotificationTask types used by the FormSpace system.
132
+ *
133
+ * Register these with the notification task service so an unhandled type is caught at wiring time rather
134
+ * than at the first submission.
135
+ */
136
+ export declare const ALL_FORM_SPACE_NOTIFICATION_TASK_TYPES: NotificationTaskType[];
@@ -0,0 +1,300 @@
1
+ import { type ContentTypeMimeType, type Maybe, type Milliseconds } from '@dereekb/util';
2
+ import { type FormSpaceFileSlot, type FormSpaceType } from './formspace.id';
3
+ /**
4
+ * @module formspace.type
5
+ *
6
+ * The {@link FormSpaceType} registry: the per-type upload restrictions, expiration policy, and submission
7
+ * metadata an app declares once and both the client and the server read.
8
+ *
9
+ * The type and the factory live here, in the model folder rather than in `firebase-server`, so a client-side
10
+ * pre-check rejects an oversized file with the SAME rule the server enforces authoritatively. Only the
11
+ * service INSTANCE is constructed server-side, as a NestJS provider — the same split
12
+ * {@link AppCalendarTypeConfigService} uses.
13
+ */
14
+ /**
15
+ * Who may read and remove an individual file, among the users who already reach the FormSpace itself.
16
+ *
17
+ * Narrows access; it never widens it. A caller that cannot read the space at all is refused before this is
18
+ * ever consulted, so `'space'` is not "public" — it is "whoever the space already lets in".
19
+ *
20
+ * - `'space'` — anyone holding the corresponding role on the space. The DEFAULT, and the only sensible
21
+ * answer for a single-user form, where every file was uploaded by the one person who can reach it.
22
+ * - `'uploader'` — only the user who uploaded that file, per {@link FormSpaceFile.ub}. For a SHARED space
23
+ * whose members contribute side by side rather than collaborating on one pile: a member adds their own
24
+ * photos and can take them back, and cannot read or delete anybody else's. The space's own `u` is NOT
25
+ * exempt — on a shared space `u` is whoever the space was opened for, not an administrator of its
26
+ * contents, and exempting them would quietly hand one member the whole album.
27
+ */
28
+ export type FormSpaceFileAccess = 'space' | 'uploader';
29
+ /**
30
+ * Default for {@link FormSpaceFileSlotConfig.fileAccess} and {@link FormSpaceTypeConfig.fileAccess}.
31
+ *
32
+ * `'space'`, so a type declared before per-file access existed keeps behaving exactly as it did.
33
+ */
34
+ export declare const DEFAULT_FORM_SPACE_FILE_ACCESS: FormSpaceFileAccess;
35
+ /**
36
+ * Restrictions for a single named upload slot within a {@link FormSpaceTypeConfig}.
37
+ *
38
+ * A slot is a LOGICAL position, not a file: uploading into an occupied slot supersedes what was there, so
39
+ * "one current resume" needs no extra bookkeeping.
40
+ */
41
+ export interface FormSpaceFileSlotConfig {
42
+ /**
43
+ * The slot this configuration applies to.
44
+ */
45
+ readonly slot: FormSpaceFileSlot;
46
+ /**
47
+ * Human-readable name of the slot, for tooling and logs.
48
+ */
49
+ readonly name?: Maybe<string>;
50
+ /**
51
+ * Whether the space may be submitted without a file in this slot. Defaults to false.
52
+ */
53
+ readonly required?: Maybe<boolean>;
54
+ /**
55
+ * Mime types accepted in this slot. Defaults to the type's {@link FormSpaceTypeConfig.allowedMimeTypes}.
56
+ */
57
+ readonly allowedMimeTypes?: Maybe<readonly ContentTypeMimeType[]>;
58
+ /**
59
+ * Size cap for a file in this slot. Defaults to the type's {@link FormSpaceTypeConfig.maxFileSizeBytes}.
60
+ */
61
+ readonly maxFileSizeBytes?: Maybe<number>;
62
+ /**
63
+ * How many files this slot may hold at once. Defaults to {@link DEFAULT_FORM_SPACE_SLOT_MAX_FILES}.
64
+ *
65
+ * At 1 the slot is a POSITION: a new upload supersedes whatever was there, which is the original "one
66
+ * current resume" behaviour. Above 1 it is a FOLDER: uploads accumulate, and one is refused once the
67
+ * folder is full rather than quietly evicting the oldest file the user put there.
68
+ */
69
+ readonly maxFiles?: Maybe<number>;
70
+ /**
71
+ * How many files this slot must hold before the space may be submitted.
72
+ *
73
+ * Defaults to 1 when {@link required} is true, and 0 otherwise, so `required` keeps meaning exactly what it
74
+ * meant before folders existed.
75
+ */
76
+ readonly minFiles?: Maybe<number>;
77
+ /**
78
+ * Who may read and remove an individual file in this slot.
79
+ *
80
+ * Defaults to the type's {@link FormSpaceTypeConfig.fileAccess}, which itself defaults to
81
+ * {@link DEFAULT_FORM_SPACE_FILE_ACCESS}. Narrowing it per slot is what lets one shared space hold a
82
+ * public banner everybody sees alongside a folder of each member's own documents.
83
+ */
84
+ readonly fileAccess?: Maybe<FormSpaceFileAccess>;
85
+ /**
86
+ * Whether a file here must pass validation before the space may be submitted. Defaults to false.
87
+ *
88
+ * Setting it does two things: an accepted upload enters the StorageFile processing pipeline instead of
89
+ * being stored as-is, and a file left PENDING or judged INVALID blocks submission. A type that sets this
90
+ * MUST register a validator for the slot server-side; the wiring asserts that at boot.
91
+ */
92
+ readonly validationRequired?: Maybe<boolean>;
93
+ }
94
+ /**
95
+ * Upload, expiration, and submission configuration for a single {@link FormSpaceType}.
96
+ *
97
+ * This is the contract a FormSpace of the type is held to. It is pure data and is shared by the client and
98
+ * the server; the server-side PROCESSING of a submission is registered separately, as a
99
+ * `FormSpaceSubmissionProcessorConfig` keyed by the same type string.
100
+ */
101
+ export interface FormSpaceTypeConfig {
102
+ /**
103
+ * The type this configuration applies to.
104
+ */
105
+ readonly formSpaceType: FormSpaceType;
106
+ /**
107
+ * Human-readable name of the type, for tooling and logs.
108
+ */
109
+ readonly name?: Maybe<string>;
110
+ /**
111
+ * Longer description of what the type collects.
112
+ */
113
+ readonly description?: Maybe<string>;
114
+ /**
115
+ * The upload slots this type declares.
116
+ *
117
+ * When empty, the type accepts no uploads at all — a FormSpace can be a pure JSON container.
118
+ */
119
+ readonly slots?: Maybe<readonly FormSpaceFileSlotConfig[]>;
120
+ /**
121
+ * Whether a slot NOT declared in {@link slots} may be uploaded into. Defaults to false.
122
+ *
123
+ * False is the safe default: an undeclared slot has no size or mime restriction of its own, so allowing
124
+ * one silently widens the type's contract to the global defaults.
125
+ */
126
+ readonly allowUndeclaredSlots?: Maybe<boolean>;
127
+ /**
128
+ * Maximum number of uploads a single FormSpace of this type may ACCEPT over its whole lifetime.
129
+ *
130
+ * Counted against the space's monotonic `uc` counter, which is why superseding a slot still consumes one:
131
+ * the cap bounds work done, not files retained. Defaults to {@link DEFAULT_FORM_SPACE_MAX_UPLOADS}.
132
+ */
133
+ readonly maxUploads?: Maybe<number>;
134
+ /**
135
+ * Mime types accepted by any slot that does not narrow them further.
136
+ * Defaults to {@link DEFAULT_FORM_SPACE_ALLOWED_MIME_TYPES}.
137
+ */
138
+ readonly allowedMimeTypes?: Maybe<readonly ContentTypeMimeType[]>;
139
+ /**
140
+ * Size cap for any slot that does not narrow it further.
141
+ * Defaults to {@link DEFAULT_FORM_SPACE_MAX_FILE_SIZE_BYTES}.
142
+ */
143
+ readonly maxFileSizeBytes?: Maybe<number>;
144
+ /**
145
+ * Who may read and remove an individual file in any slot that does not narrow it further.
146
+ * Defaults to {@link DEFAULT_FORM_SPACE_FILE_ACCESS}.
147
+ */
148
+ readonly fileAccess?: Maybe<FormSpaceFileAccess>;
149
+ /**
150
+ * How long a newly created FormSpace of this type stays editable before the expiration sweep retires it.
151
+ *
152
+ * When absent the space never expires and `eat` is never written, which is exactly what keeps it out of
153
+ * the sweep's inequality query.
154
+ */
155
+ readonly expiresIn?: Maybe<Milliseconds>;
156
+ /**
157
+ * How long after EACH submission a space of this type may be reopened, returning it to an editable draft.
158
+ *
159
+ * THE MASTER SWITCH. When absent the type is never reopenable and submission stays the one-way door it
160
+ * has always been — which is what makes the whole reopen feature opt-in, and why no existing type can
161
+ * acquire resubmit semantics by accident.
162
+ *
163
+ * Distinct from {@link expiresIn}, which bounds the lifetime of the DRAFT. This bounds the lifetime of
164
+ * the SUBMISSION's mutability.
165
+ *
166
+ * Rolling by default: absent {@link reopenableUntil}, every submission grants a fresh window measured
167
+ * from its own `sat`.
168
+ */
169
+ readonly reopenableFor?: Maybe<Milliseconds>;
170
+ /**
171
+ * A hard ceiling on reopening, measured from the FIRST submission rather than the current one.
172
+ *
173
+ * Materialized onto the space as `lat` on its first submit and never moved, so repeated reopen/resubmit
174
+ * rounds cannot walk the deadline forward the way a purely rolling {@link reopenableFor} lets them. Use
175
+ * it when "this submission is final N hours after it was first made" has to be a wall-clock guarantee.
176
+ *
177
+ * Absent leaves {@link reopenableFor} rolling. Meaningless on its own: {@link reopenableFor} is what
178
+ * permits a reopen at all, and this only narrows it.
179
+ */
180
+ readonly reopenableUntil?: Maybe<Milliseconds>;
181
+ /**
182
+ * How many times a space of this type may be reopened. Counted against the space's monotonic `rc`.
183
+ *
184
+ * Absent leaves the count unbounded and the windows above the only limit — which is fine for a
185
+ * {@link reopenableUntil} ceiling, and worth setting deliberately for a purely rolling window, where a
186
+ * resubmit keeps earning a new one.
187
+ */
188
+ readonly maxReopens?: Maybe<number>;
189
+ }
190
+ /**
191
+ * Default for {@link FormSpaceTypeConfig.maxUploads}.
192
+ */
193
+ export declare const DEFAULT_FORM_SPACE_MAX_UPLOADS = 20;
194
+ /**
195
+ * Default for {@link FormSpaceFileSlotConfig.maxFiles}.
196
+ *
197
+ * One, so a slot declared before folders existed keeps superseding rather than silently accumulating.
198
+ */
199
+ export declare const DEFAULT_FORM_SPACE_SLOT_MAX_FILES = 1;
200
+ /**
201
+ * Default for {@link FormSpaceTypeConfig.maxFileSizeBytes}: 10 MiB.
202
+ */
203
+ export declare const DEFAULT_FORM_SPACE_MAX_FILE_SIZE_BYTES: number;
204
+ /**
205
+ * Default for {@link FormSpaceTypeConfig.allowedMimeTypes}.
206
+ *
207
+ * Deliberately narrow: a form attachment is a document or an image, and an open list on the DEFAULT path is
208
+ * how an upload endpoint becomes a general-purpose file host.
209
+ */
210
+ export declare const DEFAULT_FORM_SPACE_ALLOWED_MIME_TYPES: readonly ContentTypeMimeType[];
211
+ /**
212
+ * Default expiration applied to {@link DEFAULT_FORM_SPACE_TYPE_CONFIG}-shaped types that opt in: seven days.
213
+ */
214
+ export declare const DEFAULT_FORM_SPACE_EXPIRES_IN: Milliseconds;
215
+ /**
216
+ * The {@link FormSpaceType} of {@link DEFAULT_FORM_SPACE_TYPE_CONFIG}, used for a type the app never registered.
217
+ */
218
+ export declare const UNKNOWN_FORM_SPACE_TYPE: FormSpaceType;
219
+ /**
220
+ * The configuration applied to a FormSpace whose type the app did not register.
221
+ *
222
+ * An unregistered type falls back rather than throwing on purpose: a scheduled sweep over every FormSpace in
223
+ * the app must not be taken down by one badly-typed document. CREATION is the opposite — `createFormSpace`
224
+ * rejects an unregistered type outright, so this fallback only ever governs a document that already exists.
225
+ *
226
+ * It declares no slots and does not allow undeclared ones, so an unregistered space accepts no uploads.
227
+ */
228
+ export declare const DEFAULT_FORM_SPACE_TYPE_CONFIG: FormSpaceTypeConfig;
229
+ /**
230
+ * Record of {@link FormSpaceTypeConfig} keyed by {@link FormSpaceType}.
231
+ */
232
+ export type FormSpaceTypeConfigRecord = Record<FormSpaceType, FormSpaceTypeConfig>;
233
+ /**
234
+ * Creates a {@link FormSpaceTypeConfigRecord} from an array of configs.
235
+ *
236
+ * @param configs - The configs to index.
237
+ * @returns A record keyed by form space type.
238
+ * @throws {Error} When two configs declare the same {@link FormSpaceType}.
239
+ *
240
+ * @example
241
+ * ```ts
242
+ * const record = formSpaceTypeConfigRecord([{ formSpaceType: 'demo_example' }]);
243
+ * ```
244
+ */
245
+ export declare function formSpaceTypeConfigRecord(configs: FormSpaceTypeConfig[]): FormSpaceTypeConfigRecord;
246
+ /**
247
+ * Runtime service for resolving a {@link FormSpaceTypeConfig} from a {@link FormSpaceType}.
248
+ *
249
+ * Built from a {@link FormSpaceTypeConfigRecord} via {@link appFormSpaceTypeConfigService}.
250
+ */
251
+ export declare abstract class AppFormSpaceTypeConfigService {
252
+ /**
253
+ * All registered configs for this app.
254
+ */
255
+ abstract readonly appFormSpaceTypeConfigRecord: FormSpaceTypeConfigRecord;
256
+ /**
257
+ * Returns the config for the given type, falling back to the service's default when it is not registered.
258
+ *
259
+ * @param formSpaceType - The type to look up.
260
+ */
261
+ abstract configForFormSpaceType(formSpaceType: FormSpaceType): FormSpaceTypeConfig;
262
+ /**
263
+ * Returns the config for the given type, or null when it is not registered.
264
+ *
265
+ * This is what `createFormSpace` gates on: a space may only be CREATED for a type the app declared.
266
+ *
267
+ * @param formSpaceType - The type to look up.
268
+ */
269
+ abstract registeredConfigForFormSpaceType(formSpaceType: FormSpaceType): Maybe<FormSpaceTypeConfig>;
270
+ /**
271
+ * Returns every registered {@link FormSpaceType}.
272
+ */
273
+ abstract getAllKnownFormSpaceTypes(): FormSpaceType[];
274
+ /**
275
+ * Returns every registered {@link FormSpaceTypeConfig}.
276
+ */
277
+ abstract getAllKnownFormSpaceTypeConfigs(): FormSpaceTypeConfig[];
278
+ }
279
+ /**
280
+ * Reference to an {@link AppFormSpaceTypeConfigService} instance, for dependency injection.
281
+ */
282
+ export interface AppFormSpaceTypeConfigServiceRef {
283
+ readonly appFormSpaceTypeConfigService: AppFormSpaceTypeConfigService;
284
+ }
285
+ /**
286
+ * Creates an {@link AppFormSpaceTypeConfigService} from the given record.
287
+ *
288
+ * @param appFormSpaceTypeConfigRecord - The complete form space type registry for the application.
289
+ * @param defaultConfig - Config used for an unregistered type. Defaults to {@link DEFAULT_FORM_SPACE_TYPE_CONFIG}.
290
+ * @returns The service.
291
+ *
292
+ * @example
293
+ * ```ts
294
+ * const service = appFormSpaceTypeConfigService(formSpaceTypeConfigRecord(DEMO_FORM_SPACE_TYPE_CONFIGS));
295
+ * const config = service.configForFormSpaceType('demo_example');
296
+ * ```
297
+ *
298
+ * @__NO_SIDE_EFFECTS__
299
+ */
300
+ export declare function appFormSpaceTypeConfigService(appFormSpaceTypeConfigRecord: FormSpaceTypeConfigRecord, defaultConfig?: FormSpaceTypeConfig): AppFormSpaceTypeConfigService;