@haverstack/core 0.8.0 → 0.9.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.
- package/README.md +30 -14
- package/dist/access.d.ts +20 -7
- package/dist/access.d.ts.map +1 -1
- package/dist/access.js +37 -16
- package/dist/access.js.map +1 -1
- package/dist/adapter-entry.d.ts +12 -0
- package/dist/adapter-entry.d.ts.map +1 -0
- package/dist/adapter-entry.js +11 -0
- package/dist/adapter-entry.js.map +1 -0
- package/dist/attachment-download.d.ts +75 -0
- package/dist/attachment-download.d.ts.map +1 -0
- package/dist/attachment-download.js +128 -0
- package/dist/attachment-download.js.map +1 -0
- package/dist/auth.d.ts +88 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +145 -0
- package/dist/auth.js.map +1 -0
- package/dist/combine.d.ts.map +1 -1
- package/dist/combine.js +20 -3
- package/dist/combine.js.map +1 -1
- package/dist/did-entry.d.ts +12 -0
- package/dist/did-entry.d.ts.map +1 -0
- package/dist/did-entry.js +16 -0
- package/dist/did-entry.js.map +1 -0
- package/dist/did.d.ts +51 -0
- package/dist/did.d.ts.map +1 -0
- package/dist/did.js +153 -0
- package/dist/did.js.map +1 -0
- package/dist/id.d.ts +18 -6
- package/dist/id.d.ts.map +1 -1
- package/dist/id.js +39 -10
- package/dist/id.js.map +1 -1
- package/dist/index.d.ts +15 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -9
- package/dist/index.js.map +1 -1
- package/dist/merge.d.ts +11 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +38 -0
- package/dist/merge.js.map +1 -0
- package/dist/schema.d.ts +31 -9
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +134 -11
- package/dist/schema.js.map +1 -1
- package/dist/stack.d.ts +748 -152
- package/dist/stack.d.ts.map +1 -1
- package/dist/stack.js +1805 -328
- package/dist/stack.js.map +1 -1
- package/dist/testing.d.ts +84 -23
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +260 -19
- package/dist/testing.js.map +1 -1
- package/dist/types.d.ts +281 -31
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -1
- package/dist/validate.d.ts +26 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +51 -1
- package/dist/validate.js.map +1 -1
- package/dist/wire-entry.d.ts +17 -0
- package/dist/wire-entry.d.ts.map +1 -0
- package/dist/wire-entry.js +24 -0
- package/dist/wire-entry.js.map +1 -0
- package/package.json +13 -1
package/dist/stack.d.ts
CHANGED
|
@@ -7,51 +7,240 @@
|
|
|
7
7
|
* - ID generation
|
|
8
8
|
* - Type definition and schema hashing
|
|
9
9
|
* - Content validation on write
|
|
10
|
-
* - Migration registry and
|
|
10
|
+
* - Migration registry and explicit, owner-driven migrateAll()
|
|
11
11
|
* - Version snapshotting on update
|
|
12
12
|
* - Soft and hard delete
|
|
13
13
|
*
|
|
14
14
|
* Apps should never talk to a StackAdapter directly.
|
|
15
15
|
*/
|
|
16
|
+
import type { SchemaDriftViolation } from './schema.js';
|
|
16
17
|
import type { ValidationError } from './validate.js';
|
|
17
|
-
import type { StackRecord, StackType, TypeSchema, TypeId, StackAdapter, StackQuery, QueryResult, Association, Permission, Migration, RecordVersion, StackFeatures, GrantAction } from './types.js';
|
|
18
|
+
import type { StackRecord, StackType, TypeSchema, TypeId, StackAdapter, StackQuery, RecordFilter, QueryResult, Association, Permission, Migration, RecordVersion, StackFeatures, GrantAction, AttachmentContent, EntityId, AppId, TokenSession } from './types.js';
|
|
18
19
|
export type CreateRecordOptions = {
|
|
20
|
+
/**
|
|
21
|
+
* Client-minted record ID. Must be 12 lowercase Crockford base-32
|
|
22
|
+
* characters and may not use the reserved `_` prefix. Omit to let the
|
|
23
|
+
* library generate one. See Stack.create() and ScopedStack.create()
|
|
24
|
+
* for the validation each applies.
|
|
25
|
+
*/
|
|
26
|
+
id?: string;
|
|
19
27
|
parentId?: string;
|
|
20
|
-
entityId?:
|
|
21
|
-
|
|
28
|
+
entityId?: EntityId;
|
|
29
|
+
/** Reverse-DNS identifier of the writing software — see AppId. */
|
|
30
|
+
appId?: AppId;
|
|
31
|
+
/**
|
|
32
|
+
* The authenticated principal, when it isn't the author. ScopedStack sets
|
|
33
|
+
* this from its own principal; callers of plain Stack supply it only when
|
|
34
|
+
* reconstructing a delegated write. See StackRecord.principalId.
|
|
35
|
+
*/
|
|
36
|
+
principalId?: EntityId;
|
|
22
37
|
permissions?: Permission[];
|
|
23
38
|
associations?: Association[];
|
|
24
39
|
};
|
|
40
|
+
export type ScopedStackOptions = {
|
|
41
|
+
/**
|
|
42
|
+
* The entity a delegated app acts for. Omit when the principal acts as
|
|
43
|
+
* itself — the case for an app riding its user's identity, which needs
|
|
44
|
+
* no delegation. See docs/spec/identity.md § App.
|
|
45
|
+
*/
|
|
46
|
+
onBehalfOf?: EntityId;
|
|
47
|
+
};
|
|
48
|
+
export type StackOptions = {
|
|
49
|
+
/**
|
|
50
|
+
* Ensures the owner's own `_entity` profile record exists, creating it on
|
|
51
|
+
* first run. Idempotent — safe to pass on every open. See
|
|
52
|
+
* docs/spec/identity.md § Entity.
|
|
53
|
+
*/
|
|
54
|
+
ownerProfile?: {
|
|
55
|
+
name: string;
|
|
56
|
+
handle?: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Clock-skew tolerance (ms) for the timestamp-prefix check
|
|
60
|
+
* ScopedStack.create() runs on grantee-supplied IDs; unscoped
|
|
61
|
+
* Stack.create() never runs it. Default: 24 hours; null disables.
|
|
62
|
+
* See docs/spec/data-model.md § Record IDs.
|
|
63
|
+
*/
|
|
64
|
+
idTimestampSkewMs?: number | null;
|
|
65
|
+
};
|
|
66
|
+
export type CollectAttachmentGarbageOptions = {
|
|
67
|
+
/**
|
|
68
|
+
* How recent an unreferenced file must be to survive collection, covering
|
|
69
|
+
* the upload-then-associate window. Default: 24 hours. Pass 0 to collect
|
|
70
|
+
* anything unreferenced right now.
|
|
71
|
+
*/
|
|
72
|
+
graceMs?: number;
|
|
73
|
+
/** Compute what would be deleted without deleting anything. Default: false. */
|
|
74
|
+
dryRun?: boolean;
|
|
75
|
+
};
|
|
76
|
+
export type CollectAttachmentGarbageResult = {
|
|
77
|
+
deleted: string[];
|
|
78
|
+
reclaimedBytes: number;
|
|
79
|
+
};
|
|
25
80
|
export type GetRecordOptions = {
|
|
26
|
-
/**
|
|
27
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Records are returned exactly as stored by default. Pass "latest" to
|
|
83
|
+
* apply the registered migration chain in memory — never written back.
|
|
84
|
+
* See docs/spec/data-model.md § Type migrations.
|
|
85
|
+
*/
|
|
86
|
+
presentAt?: 'stored' | 'latest';
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Opt-in optimistic-concurrency precondition, accepted by every mutation
|
|
90
|
+
* that bumps a record's version. On mismatch the mutation throws
|
|
91
|
+
* StackVersionConflictError and changes nothing; omit to keep
|
|
92
|
+
* last-writer-wins. See docs/spec/versioning.md § Optimistic concurrency (`ifVersion`).
|
|
93
|
+
*/
|
|
94
|
+
export type IfVersionOptions = {
|
|
95
|
+
ifVersion?: number;
|
|
28
96
|
};
|
|
29
|
-
export type DeleteRecordOptions = {
|
|
97
|
+
export type DeleteRecordOptions = IfVersionOptions & {
|
|
30
98
|
/** If true, permanently remove the record and all its history. Default: false */
|
|
31
99
|
hard?: boolean;
|
|
32
100
|
};
|
|
33
101
|
export type DefineTypeOptions = {
|
|
34
102
|
migratesFrom?: TypeId;
|
|
35
103
|
};
|
|
36
|
-
|
|
104
|
+
/**
|
|
105
|
+
* The wire-protocol discriminator vocabulary, one code per Stack-domain
|
|
106
|
+
* error class. Lives here rather than in @haverstack/wire-types because the
|
|
107
|
+
* classes that carry these codes are defined here; wire-types re-exports it
|
|
108
|
+
* as WireErrorCode. See docs/spec/wire-format.md § Wire error body.
|
|
109
|
+
*/
|
|
110
|
+
export type StackErrorCode = 'bad_request' | 'permission' | 'not_found' | 'conflict' | 'version_conflict' | 'validation' | 'migration' | 'schema_drift' | 'payload_too_large' | 'timeout';
|
|
111
|
+
/**
|
|
112
|
+
* Root of the Stack error taxonomy. A single `instanceof StackError` answers
|
|
113
|
+
* "is this a Stack-domain error or a bug?" — the question a server's error
|
|
114
|
+
* middleware asks before serializing a wire body, and one a nine-arm
|
|
115
|
+
* instanceof ladder answers only by exhaustion. Every subclass carries its
|
|
116
|
+
* discriminator as an instance `code`, so serialization is a lookup rather
|
|
117
|
+
* than a chain of class tests.
|
|
118
|
+
*
|
|
119
|
+
* Membership implies a wire mapping: every code has an entry in
|
|
120
|
+
* WIRE_ERROR_STATUS. Errors with no wire representation (IdGenerationError,
|
|
121
|
+
* InvalidDidError) deliberately stay outside this hierarchy.
|
|
122
|
+
*
|
|
123
|
+
* Subclassing adds no hierarchy beyond this root — notably
|
|
124
|
+
* StackVersionConflictError is a sibling of StackConflictError, not a
|
|
125
|
+
* subtype. See docs/spec/wire-format.md § Error responses.
|
|
126
|
+
*/
|
|
127
|
+
export declare abstract class StackError extends Error {
|
|
128
|
+
abstract readonly code: StackErrorCode;
|
|
129
|
+
}
|
|
130
|
+
export declare class StackValidationError extends StackError {
|
|
37
131
|
readonly errors: ValidationError[];
|
|
132
|
+
static readonly code: "validation";
|
|
133
|
+
readonly code: "validation";
|
|
38
134
|
constructor(errors: ValidationError[]);
|
|
39
135
|
}
|
|
40
|
-
export declare class StackMigrationError extends
|
|
136
|
+
export declare class StackMigrationError extends StackError {
|
|
137
|
+
static readonly code: "migration";
|
|
138
|
+
readonly code: "migration";
|
|
41
139
|
constructor(message: string);
|
|
42
140
|
}
|
|
43
141
|
/** Thrown by ScopedStack when a requester lacks permission for the operation. */
|
|
44
|
-
export declare class StackPermissionError extends
|
|
142
|
+
export declare class StackPermissionError extends StackError {
|
|
143
|
+
static readonly code: "permission";
|
|
144
|
+
readonly code: "permission";
|
|
45
145
|
constructor(message?: string);
|
|
46
146
|
}
|
|
47
147
|
/** Thrown when a record (or specific version) does not exist. */
|
|
48
|
-
export declare class StackNotFoundError extends
|
|
148
|
+
export declare class StackNotFoundError extends StackError {
|
|
149
|
+
static readonly code: "not_found";
|
|
150
|
+
readonly code: "not_found";
|
|
49
151
|
constructor(message: string);
|
|
50
152
|
}
|
|
51
153
|
/** Thrown when an operation cannot proceed due to a constraint violation (e.g. deleting an attachment that is still referenced). */
|
|
52
|
-
export declare class StackConflictError extends
|
|
154
|
+
export declare class StackConflictError extends StackError {
|
|
155
|
+
static readonly code: "conflict";
|
|
156
|
+
readonly code: "conflict";
|
|
157
|
+
constructor(message: string);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Thrown when an `ifVersion` precondition doesn't match a record's current
|
|
161
|
+
* version. Deliberately not a StackConflictError subtype — the two have
|
|
162
|
+
* different recovery stories and HTTP statuses (409 vs. 412). See
|
|
163
|
+
* docs/spec/versioning.md § Optimistic concurrency (`ifVersion`).
|
|
164
|
+
*/
|
|
165
|
+
export declare class StackVersionConflictError extends StackError {
|
|
166
|
+
readonly recordId: string;
|
|
167
|
+
readonly expectedVersion: number;
|
|
168
|
+
readonly actualVersion: number;
|
|
169
|
+
static readonly code: "version_conflict";
|
|
170
|
+
readonly code: "version_conflict";
|
|
171
|
+
constructor(message: string, recordId: string, expectedVersion: number, actualVersion: number);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Thrown when a request is structurally malformed — not a content-validation
|
|
175
|
+
* failure, but input the adapter/server can't even interpret (e.g. an
|
|
176
|
+
* undecodable pagination cursor). Distinct from StackValidationError, which
|
|
177
|
+
* means the request was well-formed but content failed schema validation.
|
|
178
|
+
*/
|
|
179
|
+
export declare class StackQueryError extends StackError {
|
|
180
|
+
static readonly code: "bad_request";
|
|
181
|
+
readonly code: "bad_request";
|
|
182
|
+
constructor(message: string);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Fail loud rather than silently widen: a filter the adapter can't honor
|
|
186
|
+
* would otherwise be dropped, returning an unfiltered superset presented
|
|
187
|
+
* as the filtered result. Shared by Stack.query() and
|
|
188
|
+
* APIAdapter.queryRecords(). See docs/spec/data-model.md
|
|
189
|
+
* § Capability-gated filters.
|
|
190
|
+
*/
|
|
191
|
+
export declare function assertQueryCapabilities(filter: RecordFilter | undefined, capabilities: Pick<StackFeatures, 'fullTextSearch' | 'contentFieldQuery'>): void;
|
|
192
|
+
/**
|
|
193
|
+
* Thrown when an attachment upload exceeds the adapter's declared
|
|
194
|
+
* `maxAttachmentBytes` ceiling — checked client-side before any bytes are
|
|
195
|
+
* sent; a server still enforces 413 authoritatively regardless. See
|
|
196
|
+
* docs/spec/wire-format.md § Attachments.
|
|
197
|
+
*/
|
|
198
|
+
export declare class StackPayloadTooLargeError extends StackError {
|
|
199
|
+
static readonly code: "payload_too_large";
|
|
200
|
+
readonly code: "payload_too_large";
|
|
53
201
|
constructor(message: string);
|
|
54
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Thrown when a server abandons an operation for taking too long — in
|
|
205
|
+
* practice a full-text search, the one query whose cost the sanitizers
|
|
206
|
+
* bound the *grammar* of but not the execution of (see
|
|
207
|
+
* docs/spec/data-model.md § Capability-gated filters).
|
|
208
|
+
*
|
|
209
|
+
* Never produced in-process: both SQLite engines run synchronously, so
|
|
210
|
+
* there is nothing to interrupt from inside the call. It exists so a
|
|
211
|
+
* server that bounds query time has a class to serialize, and so the app
|
|
212
|
+
* catching it can tell "too expensive, narrow it and retry" from
|
|
213
|
+
* StackQueryError's "malformed, don't bother retrying" — the distinction
|
|
214
|
+
* that would be lost if a timeout reused `bad_request`.
|
|
215
|
+
*/
|
|
216
|
+
export declare class StackTimeoutError extends StackError {
|
|
217
|
+
static readonly code: "timeout";
|
|
218
|
+
readonly code: "timeout";
|
|
219
|
+
constructor(message: string);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Thrown by defineType() when redefining an existing typeId with a schema
|
|
223
|
+
* change beyond additive evolution. The remedy is a new version, never an
|
|
224
|
+
* in-place redefinition. See docs/spec/data-model.md § Schema drift
|
|
225
|
+
* detection.
|
|
226
|
+
*/
|
|
227
|
+
export declare class StackSchemaDriftError extends StackError {
|
|
228
|
+
readonly typeId: TypeId;
|
|
229
|
+
readonly violations: SchemaDriftViolation[];
|
|
230
|
+
static readonly code: "schema_drift";
|
|
231
|
+
readonly code: "schema_drift";
|
|
232
|
+
constructor(typeId: TypeId, violations: SchemaDriftViolation[]);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Thrown when a Stack or ScopedStack is used after close(). Deliberately
|
|
236
|
+
* outside the StackError taxonomy, alongside IdGenerationError and
|
|
237
|
+
* InvalidDidError: a caller holding a closed client is a local programming
|
|
238
|
+
* error with no wire representation — no server ever responds with it.
|
|
239
|
+
* See docs/spec/adapters.md § Lifecycle.
|
|
240
|
+
*/
|
|
241
|
+
export declare class StackClosedError extends Error {
|
|
242
|
+
constructor(message?: string);
|
|
243
|
+
}
|
|
55
244
|
/**
|
|
56
245
|
* The app-facing record API, implemented by both Stack and ScopedStack.
|
|
57
246
|
* Accept this type in plugin or extension code to remain adapter-agnostic
|
|
@@ -64,48 +253,97 @@ export interface StackClient {
|
|
|
64
253
|
}>;
|
|
65
254
|
get(id: string, opts?: GetRecordOptions): Promise<StackRecord | null>;
|
|
66
255
|
query(query?: StackQuery): Promise<QueryResult>;
|
|
67
|
-
update(id: string, content: Record<string, unknown | null
|
|
68
|
-
associate(id: string, association: Association): Promise<void>;
|
|
69
|
-
dissociate(id: string, association: Association): Promise<void>;
|
|
70
|
-
setPermissions(id: string, permissions: Permission[]): Promise<void>;
|
|
256
|
+
update(id: string, content: Record<string, unknown | null>, opts?: IfVersionOptions): Promise<StackRecord>;
|
|
257
|
+
associate(id: string, association: Association, opts?: IfVersionOptions): Promise<void>;
|
|
258
|
+
dissociate(id: string, association: Association, opts?: IfVersionOptions): Promise<void>;
|
|
259
|
+
setPermissions(id: string, permissions: Permission[], opts?: IfVersionOptions): Promise<void>;
|
|
71
260
|
delete(id: string, opts?: DeleteRecordOptions): Promise<void>;
|
|
261
|
+
undelete(id: string, opts?: IfVersionOptions): Promise<StackRecord>;
|
|
72
262
|
getVersions(id: string): Promise<RecordVersion[]>;
|
|
73
263
|
getVersion(id: string, version: number): Promise<RecordVersion | null>;
|
|
74
|
-
restoreVersion(id: string, version: number): Promise<StackRecord>;
|
|
264
|
+
restoreVersion(id: string, version: number, opts?: IfVersionOptions): Promise<StackRecord>;
|
|
75
265
|
getAttachment(fileId: string): Promise<Uint8Array>;
|
|
76
|
-
putAttachment(data: Uint8Array, mimeType: string, filename?: string): Promise<
|
|
266
|
+
putAttachment(data: Uint8Array, mimeType: string, filename?: string, appId?: AppId): Promise<StackRecord & {
|
|
267
|
+
content: AttachmentContent;
|
|
268
|
+
}>;
|
|
77
269
|
deleteAttachment(fileId: string): Promise<void>;
|
|
270
|
+
collectAttachmentGarbage(opts?: CollectAttachmentGarbageOptions): Promise<CollectAttachmentGarbageResult>;
|
|
78
271
|
}
|
|
79
272
|
export declare class Stack implements StackClient {
|
|
80
273
|
private readonly adapter;
|
|
274
|
+
private readonly idTimestampSkewMsValue;
|
|
81
275
|
private readonly migrations;
|
|
276
|
+
/**
|
|
277
|
+
* Highest version this instance has defineType()'d, per baseId — what
|
|
278
|
+
* this app process understands, as distinct from what exists in shared
|
|
279
|
+
* storage. Used to detect the stale-writer case in presentAtLatest().
|
|
280
|
+
*/
|
|
281
|
+
private readonly maxDefinedVersion;
|
|
282
|
+
/**
|
|
283
|
+
* Types this instance has fetched or defined, keyed by versioned id. A
|
|
284
|
+
* Type's schema is immutable once defined, so entries are never
|
|
285
|
+
* invalidated, only added; listTypes() refreshes wholesale. See
|
|
286
|
+
* docs/spec/data-model.md § Type cache.
|
|
287
|
+
*/
|
|
288
|
+
private readonly typeCache;
|
|
289
|
+
/** Set by close(). See docs/spec/adapters.md § Lifecycle. */
|
|
290
|
+
private closed;
|
|
82
291
|
private constructor();
|
|
292
|
+
private getTypeCached;
|
|
83
293
|
/**
|
|
84
294
|
* Create a Stack instance. Reads ownerEntityId and timezone from the adapter.
|
|
85
295
|
*/
|
|
86
|
-
static create(adapter: StackAdapter): Promise<Stack>;
|
|
87
|
-
|
|
88
|
-
|
|
296
|
+
static create(adapter: StackAdapter, opts?: StackOptions): Promise<Stack>;
|
|
297
|
+
/**
|
|
298
|
+
* Idempotent bootstrap for StackOptions.ownerProfile. Filters by family
|
|
299
|
+
* only (contentFieldQuery is capability-gated) and matches `content.did`
|
|
300
|
+
* in memory, cursor-walking so an owner card past page one isn't missed
|
|
301
|
+
* and duplicated. The probe must be blind to nothing the binding rules
|
|
302
|
+
* see, or it mints a card they then refuse and the stack won't open with
|
|
303
|
+
* `ownerProfile`: soft-deleted cards still reserve their `did`, and a card
|
|
304
|
+
* migrated to a later version still holds one.
|
|
305
|
+
* See docs/spec/identity.md § DID bindings.
|
|
306
|
+
*/
|
|
307
|
+
private ensureOwnerEntity;
|
|
308
|
+
get ownerEntityId(): EntityId;
|
|
309
|
+
get timezone(): string | undefined;
|
|
89
310
|
get features(): StackFeatures;
|
|
90
311
|
/**
|
|
91
|
-
* Get a permission-scoped view of this Stack, as if
|
|
92
|
-
* the given
|
|
93
|
-
*
|
|
94
|
-
*
|
|
312
|
+
* Get a permission-scoped view of this Stack, as if requests came from
|
|
313
|
+
* the given principal (null = anonymous). Plain Stack methods are
|
|
314
|
+
* unscoped; use asEntity() when one Stack serves multiple, possibly
|
|
315
|
+
* untrusted, entities.
|
|
95
316
|
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* multi-tenant API server).
|
|
317
|
+
* `onBehalfOf` names the subject a delegated app acts for: authority is
|
|
318
|
+
* then the intersection of both parties' grants, while authorship and
|
|
319
|
+
* `-own` resolve against the subject. See
|
|
320
|
+
* docs/spec/access-control.md § Enforcement: Stack.asEntity().
|
|
101
321
|
*/
|
|
102
|
-
asEntity(entityId:
|
|
322
|
+
asEntity(entityId: EntityId | null, opts?: ScopedStackOptions): ScopedStack;
|
|
103
323
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
324
|
+
* Scope to an authenticated session — what StackTokenStore.lookupToken()
|
|
325
|
+
* returns. Equivalent to asEntity(principalId, { onBehalfOf: subjectId }),
|
|
326
|
+
* and what a server should reach for at its request boundary.
|
|
327
|
+
*
|
|
328
|
+
* Both identities are DIDs, so passing them positionally leaves nothing
|
|
329
|
+
* to catch a swap, and a swapped pair is undetectable in the undelegated
|
|
330
|
+
* case where they are equal — it would surface only once delegation is in
|
|
331
|
+
* use, as authority no longer fenced by the app's grants and every write
|
|
332
|
+
* attributed to the app rather than the person. Taking the pair whole
|
|
333
|
+
* removes the order to get wrong. See
|
|
334
|
+
* docs/spec/access-control.md § Delegation: principal and subject.
|
|
335
|
+
*/
|
|
336
|
+
forSession(session: TokenSession): ScopedStack;
|
|
337
|
+
/**
|
|
338
|
+
* Define and persist a Type; call at app startup before creating records
|
|
339
|
+
* of the type. Redefining an existing typeId is checked against the
|
|
340
|
+
* stored schema: identical is a no-op, a name-only change persists, and
|
|
341
|
+
* anything beyond additive evolution throws StackSchemaDriftError. See
|
|
342
|
+
* docs/spec/data-model.md § Schema drift detection.
|
|
106
343
|
*/
|
|
107
344
|
defineType(id: TypeId, name: string, schema: TypeSchema, opts?: DefineTypeOptions): Promise<StackType>;
|
|
108
345
|
getType(id: TypeId): Promise<StackType | null>;
|
|
346
|
+
/** Refreshes typeCache wholesale — the explicit way to see a rename made by another writer. */
|
|
109
347
|
listTypes(): Promise<StackType[]>;
|
|
110
348
|
/**
|
|
111
349
|
* Check whether a record's type is compatible with a required schema.
|
|
@@ -113,12 +351,10 @@ export declare class Stack implements StackClient {
|
|
|
113
351
|
*/
|
|
114
352
|
typeIsCompatible(typeId: TypeId, requiredSchema: TypeSchema): Promise<boolean>;
|
|
115
353
|
/**
|
|
116
|
-
* Register a migration function between two adjacent Type versions
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* Migrations run in-memory — they do not write to the adapter unless
|
|
121
|
-
* you call migrateAll() explicitly.
|
|
354
|
+
* Register a migration function between two adjacent Type versions; call
|
|
355
|
+
* at app startup after defineType(). Runs in-memory — nothing is written
|
|
356
|
+
* until migrateAll(). Adjacent migrations are composed into chains
|
|
357
|
+
* automatically.
|
|
122
358
|
*/
|
|
123
359
|
registerMigration(migration: Migration): void;
|
|
124
360
|
/**
|
|
@@ -132,99 +368,180 @@ export declare class Stack implements StackClient {
|
|
|
132
368
|
*/
|
|
133
369
|
private latestTypeId;
|
|
134
370
|
/**
|
|
135
|
-
* Eagerly migrate all records of a type family to the latest version
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* and written back only when next updated. Call migrateAll() when you
|
|
140
|
-
* want to commit all pending migrations in one deliberate pass, for
|
|
141
|
-
* example before a deployment or after a major schema change.
|
|
142
|
-
*
|
|
143
|
-
* Previous content is preserved in version history before each write.
|
|
371
|
+
* Eagerly migrate all records of a type family to the latest version —
|
|
372
|
+
* the only way disk state changes version. Sweeps soft-deleted records
|
|
373
|
+
* too, validates each result before writing, and aborts on the first
|
|
374
|
+
* validation failure. See docs/spec/data-model.md § Type migrations.
|
|
144
375
|
*/
|
|
145
376
|
migrateAll(baseTypeId: string): Promise<{
|
|
146
377
|
migrated: number;
|
|
147
378
|
}>;
|
|
148
379
|
/**
|
|
149
380
|
* Create a new record. Validates content against the type's schema.
|
|
381
|
+
* `_group` records get their author stamped as the first `admin` roster
|
|
382
|
+
* association here — the single stamping site for both Stack.create()
|
|
383
|
+
* and ScopedStack.create(). See docs/spec/identity.md § Group.
|
|
150
384
|
*/
|
|
151
385
|
create<T extends Record<string, unknown> = Record<string, unknown>>(typeId: TypeId, content: T, opts?: CreateRecordOptions): Promise<StackRecord & {
|
|
152
386
|
content: T;
|
|
153
387
|
}>;
|
|
154
388
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* registered
|
|
159
|
-
*
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
389
|
+
* Apply the registered migration chain in memory, for presentAt:
|
|
390
|
+
* 'latest'. Never writes back. Throws StackMigrationError when the
|
|
391
|
+
* record's version can't be reconciled with what this instance has
|
|
392
|
+
* registered — the stale-writer case. See docs/spec/data-model.md
|
|
393
|
+
* § Type migrations.
|
|
394
|
+
*/
|
|
395
|
+
private presentAtLatest;
|
|
396
|
+
/**
|
|
397
|
+
* Get a record by ID, exactly as stored — no implicit migration. Pass
|
|
398
|
+
* { presentAt: 'latest' } to migrate in memory; only migrateAll()
|
|
399
|
+
* commits migrations to disk.
|
|
163
400
|
*/
|
|
164
401
|
get(id: string, opts?: GetRecordOptions): Promise<StackRecord | null>;
|
|
165
402
|
/**
|
|
166
|
-
* Update a record's content
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* If the record is on an older type version, its content is migrated
|
|
173
|
-
* in memory first, then the patch is applied. The updated record is
|
|
174
|
-
* written at the latest type version — this is when lazy migration
|
|
175
|
-
* is committed to disk.
|
|
176
|
-
*
|
|
177
|
-
* Validates the merged result against the type's schema. Saves the
|
|
178
|
-
* previous state to version history before updating.
|
|
179
|
-
*
|
|
180
|
-
* For association or permission changes, use associate(), dissociate(),
|
|
181
|
-
* and setPermissions() instead.
|
|
403
|
+
* Update a record's content via JSON Merge Patch: omitted fields are
|
|
404
|
+
* kept, null removes a field. Validates the merged result against the
|
|
405
|
+
* record's *current* stored type and never changes typeId (see
|
|
406
|
+
* docs/spec/data-model.md § Type migrations); snapshots the prior state
|
|
407
|
+
* to version history. Associations and permissions have their own methods.
|
|
182
408
|
*/
|
|
183
|
-
update(id: string, content: Record<string, unknown | null
|
|
409
|
+
update(id: string, content: Record<string, unknown | null>, opts?: IfVersionOptions): Promise<StackRecord>;
|
|
184
410
|
/**
|
|
185
|
-
* Add an association to a record.
|
|
411
|
+
* Add an association to a record. Snapshots the record's prior state and
|
|
412
|
+
* bumps version, same as update() — associations are covered by the same
|
|
413
|
+
* versioning rule as content.
|
|
186
414
|
* If the association already exists (same kind, label, and payload), this is a no-op.
|
|
187
415
|
*/
|
|
188
|
-
associate(id: string, association: Association): Promise<void>;
|
|
416
|
+
associate(id: string, association: Association, opts?: IfVersionOptions): Promise<void>;
|
|
189
417
|
/**
|
|
190
|
-
* Remove an association from a record.
|
|
191
|
-
* Matched by kind, label, and payload. No-op if not found.
|
|
418
|
+
* Remove an association from a record. Snapshots and bumps version, same
|
|
419
|
+
* as associate(). Matched by kind, label, and payload. No-op if not found.
|
|
192
420
|
*/
|
|
193
|
-
dissociate(id: string, association: Association): Promise<void>;
|
|
421
|
+
dissociate(id: string, association: Association, opts?: IfVersionOptions): Promise<void>;
|
|
194
422
|
/**
|
|
195
|
-
* Replace all permissions on a record.
|
|
196
|
-
* Pass an empty array to make the record private (the
|
|
423
|
+
* Replace all permissions on a record. Snapshots and bumps version, same
|
|
424
|
+
* as associate(). Pass an empty array to make the record private (the
|
|
425
|
+
* default). No-op if the new set is deep-equal to the current one.
|
|
197
426
|
*/
|
|
198
|
-
setPermissions(id: string, permissions: Permission[]): Promise<void>;
|
|
427
|
+
setPermissions(id: string, permissions: Permission[], opts?: IfVersionOptions): Promise<void>;
|
|
199
428
|
/**
|
|
200
|
-
* Soft-delete a record (default) or hard-delete it permanently
|
|
201
|
-
*
|
|
202
|
-
*
|
|
429
|
+
* Soft-delete a record (default) or hard-delete it permanently, removing
|
|
430
|
+
* the record and its version history. Soft delete snapshots and bumps
|
|
431
|
+
* version; a no-op if already deleted. `_config` is never deletable
|
|
432
|
+
* (docs/spec.md § The `_config` record). See docs/spec/versioning.md
|
|
433
|
+
* § Deletion.
|
|
203
434
|
*/
|
|
204
435
|
delete(id: string, opts?: DeleteRecordOptions): Promise<void>;
|
|
205
436
|
/**
|
|
206
|
-
*
|
|
437
|
+
* Reverse a soft delete. Idempotent — undeleting a record that isn't
|
|
438
|
+
* deleted returns it unchanged. Hard-deleted records are gone, so this
|
|
439
|
+
* throws StackNotFoundError for them just like any other missing record.
|
|
440
|
+
* Snapshots and bumps version, same as delete().
|
|
441
|
+
*/
|
|
442
|
+
undelete(id: string, opts?: IfVersionOptions): Promise<StackRecord>;
|
|
443
|
+
/**
|
|
444
|
+
* Query records. filter.baseId matches every version of a type family,
|
|
445
|
+
* resolved against registered Types. Results come back exactly as
|
|
446
|
+
* stored; pass presentAt: 'latest' to migrate in memory. See
|
|
447
|
+
* docs/spec/data-model.md § Queries.
|
|
207
448
|
*/
|
|
208
449
|
query(query?: StackQuery): Promise<QueryResult>;
|
|
450
|
+
/**
|
|
451
|
+
* Resolve filter.baseId into a concrete typeId set (intersected with
|
|
452
|
+
* filter.typeId when both are given), so adapters never need their own
|
|
453
|
+
* baseId concept. Returns EMPTY_FAMILY when the resolved set is empty so
|
|
454
|
+
* the caller can short-circuit without an adapter round trip.
|
|
455
|
+
*/
|
|
456
|
+
private resolveBaseIdFilter;
|
|
209
457
|
getVersions(id: string): Promise<RecordVersion[]>;
|
|
210
458
|
getVersion(id: string, version: number): Promise<RecordVersion | null>;
|
|
211
459
|
/**
|
|
212
|
-
* Restore a record to a previous version by creating a new version
|
|
213
|
-
*
|
|
460
|
+
* Restore a record to a previous version by creating a new version —
|
|
461
|
+
* never rewrites history. The snapshot is validated against its own
|
|
462
|
+
* stored typeId (not the record's current type), restores associations,
|
|
463
|
+
* and never restores permissions. See docs/spec/versioning.md § Restore
|
|
464
|
+
* semantics.
|
|
465
|
+
*/
|
|
466
|
+
restoreVersion(id: string, version: number, opts?: IfVersionOptions): Promise<StackRecord>;
|
|
467
|
+
/** Uniqueness for every unique binding field a newly created card claims. */
|
|
468
|
+
private checkBindingsOnCreate;
|
|
469
|
+
/**
|
|
470
|
+
* Immutability for every binding field a patch touches, then uniqueness
|
|
471
|
+
* for the subset that carries it. Fields absent from the patch carry no
|
|
472
|
+
* new claim — update() is a merge, so an untouched binding is the one the
|
|
473
|
+
* card already holds.
|
|
474
|
+
*/
|
|
475
|
+
private checkBindingsOnUpdate;
|
|
476
|
+
/**
|
|
477
|
+
* A unique binding field is what a lookup resolves *by* — a record's
|
|
478
|
+
* `principalId` by `_app.did`, its `entityId` by `_entity.did`. Two cards
|
|
479
|
+
* claiming one value would leave that lookup without a single answer, and
|
|
480
|
+
* ambiguity is all an impersonating card needs. Enforced here rather than
|
|
481
|
+
* by schema, since uniqueness is a property of the set, not of the value.
|
|
482
|
+
* Called by the paths that can introduce a binding: create and update.
|
|
483
|
+
*
|
|
484
|
+
* Short-circuits on the first clash rather than materialising the family,
|
|
485
|
+
* so a stack whose `_entity` family is larger than a single scan settles
|
|
486
|
+
* the common case — the value is already taken — without walking the rest.
|
|
487
|
+
*
|
|
488
|
+
* Read-then-write, so two creates racing on one value can both pass.
|
|
489
|
+
* Closing that properly means a unique index over a JSON field, which
|
|
490
|
+
* each adapter would enforce separately — a decision about where
|
|
491
|
+
* uniqueness lives, not a local fix.
|
|
492
|
+
* See docs/spec/identity.md § DID bindings.
|
|
493
|
+
*/
|
|
494
|
+
private checkBindingUnique;
|
|
495
|
+
/**
|
|
496
|
+
* A binding is permanent once made: uniqueness stops a second card
|
|
497
|
+
* claiming a value, but only immutability stops an existing card being
|
|
498
|
+
* moved onto one, which reaches the same impersonation by another route.
|
|
499
|
+
* Adopting a value is therefore a one-way step, and a subject whose key
|
|
500
|
+
* changes gets a new card — matching identity.md's deferral of key
|
|
501
|
+
* rotation, where a new key is a new identity rather than the same one
|
|
502
|
+
* relabelled. See docs/spec/identity.md § DID bindings.
|
|
214
503
|
*/
|
|
215
|
-
|
|
504
|
+
private checkBindingImmutable;
|
|
216
505
|
/**
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
506
|
+
* mimeType is a property of the fileId, not the uploader's perspective:
|
|
507
|
+
* the first metadata record for a fileId fixes it, and a conflicting
|
|
508
|
+
* later upload is rejected. Cursor-walked so earlier records past page
|
|
509
|
+
* one are seen. See docs/spec/attachments.md § The `_attachment` record
|
|
510
|
+
* type.
|
|
511
|
+
*
|
|
512
|
+
* Best-effort by construction — check-then-create with no storage-level
|
|
513
|
+
* uniqueness behind it, so two racing first uploads can both land on a
|
|
514
|
+
* concurrent server. What survives that race is the *resolution*: which
|
|
515
|
+
* record establishes the type is firstRecordedAttachment()'s total
|
|
516
|
+
* order, the same one a server serving Content-Type applies, so both
|
|
517
|
+
* sides name the same winner however many records exist.
|
|
220
518
|
*/
|
|
221
|
-
|
|
519
|
+
private checkAttachmentMimeTypeOnCreate;
|
|
222
520
|
/**
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
521
|
+
* filename is the only mutable field on an _attachment@1 record; fileId,
|
|
522
|
+
* size, and mimeType are immutable (even a same-value mimeType rewrite
|
|
523
|
+
* is refused). The correction flow is delete + re-upload. See
|
|
524
|
+
* docs/spec/attachments.md § The `_attachment` record type.
|
|
226
525
|
*/
|
|
227
|
-
|
|
526
|
+
private checkAttachmentImmutableFields;
|
|
527
|
+
/**
|
|
528
|
+
* `_config.entityId` defines stack ownership; neither update() nor
|
|
529
|
+
* restoreVersion() may change it. A conflict with stack integrity, not a
|
|
530
|
+
* schema violation — hence StackConflictError. See docs/spec.md § The
|
|
531
|
+
* `_config` record.
|
|
532
|
+
*/
|
|
533
|
+
private checkConfigEntityIdUnchanged;
|
|
534
|
+
/**
|
|
535
|
+
* Store bytes and create an _attachment@1 metadata record (owner-
|
|
536
|
+
* attributed, no entityId), returning that record — `content.fileId`
|
|
537
|
+
* addresses the bytes, `id` addresses the metadata. Delegates to the
|
|
538
|
+
* adapter's atomic putAttachmentWithMetadata() when implemented, trusting
|
|
539
|
+
* the returned record as backend-authoritative; otherwise falls back to
|
|
540
|
+
* bytes-then-create(). See docs/spec/wire-format.md § Attachments.
|
|
541
|
+
*/
|
|
542
|
+
putAttachment(data: Uint8Array, mimeType: string, filename?: string, appId?: AppId): Promise<StackRecord & {
|
|
543
|
+
content: AttachmentContent;
|
|
544
|
+
}>;
|
|
228
545
|
getAttachment(fileId: string): Promise<Uint8Array>;
|
|
229
546
|
/**
|
|
230
547
|
* Delete an attachment's bytes and its _attachment@1 metadata record(s).
|
|
@@ -233,107 +550,381 @@ export declare class Stack implements StackClient {
|
|
|
233
550
|
*/
|
|
234
551
|
deleteAttachment(fileId: string): Promise<void>;
|
|
235
552
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
|
|
553
|
+
* Non-atomic fallback for adapters that don't implement
|
|
554
|
+
* deleteUnreferencedAttachmentRecords(): a concurrent associate() can
|
|
555
|
+
* race between the reference check below and the deletes it guards.
|
|
556
|
+
*/
|
|
557
|
+
private deleteUnreferencedAttachmentRecordsFallback;
|
|
558
|
+
/**
|
|
559
|
+
* Sweep for attachment bytes unreachable from any record — live or
|
|
560
|
+
* soft-deleted — and delete bytes + metadata. Deletion goes through
|
|
561
|
+
* deleteAttachment(), so a file re-referenced by sweep time is skipped,
|
|
562
|
+
* not a failure. See docs/spec/attachments.md § Garbage collection.
|
|
563
|
+
*/
|
|
564
|
+
collectAttachmentGarbage(opts?: CollectAttachmentGarbageOptions): Promise<CollectAttachmentGarbageResult>;
|
|
565
|
+
/**
|
|
566
|
+
* Flush pending writes to the underlying storage. A no-op for adapters
|
|
567
|
+
* that commit on every call (SQLite, the API adapter); meaningful for
|
|
568
|
+
* ones that buffer, and for checkpointing a stack that stays open —
|
|
569
|
+
* close() covers the teardown case on its own.
|
|
240
570
|
*/
|
|
241
571
|
flush(): Promise<void>;
|
|
242
572
|
/**
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
573
|
+
* Flush, then release any resources the adapter holds (connections, file
|
|
574
|
+
* handles, lock files). A failed flush still releases them before it
|
|
575
|
+
* propagates: an unwritable stack must not also leak a lock file.
|
|
576
|
+
* See docs/spec/adapters.md § Lifecycle.
|
|
247
577
|
*/
|
|
248
578
|
close(): Promise<void>;
|
|
249
579
|
/**
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
|
|
580
|
+
* Throws once close() has run. Public only so ScopedStack can gate the
|
|
581
|
+
* one path it takes to the adapter directly; not an app-facing API.
|
|
582
|
+
*/
|
|
583
|
+
assertOpen(): void;
|
|
584
|
+
/**
|
|
585
|
+
* Create _grant records authorizing entities to act on records of
|
|
586
|
+
* specific types; null entityId writes a default grant (any
|
|
587
|
+
* authenticated entity).
|
|
253
588
|
*
|
|
254
|
-
*
|
|
589
|
+
* Granting an **app** a `-own` action does not contain it the way the
|
|
590
|
+
* suffix suggests: when that app acts for someone, `-own` is read as the
|
|
591
|
+
* bare verb and the subject decides which records are in reach, so in a
|
|
592
|
+
* personal stack — where nearly every record is owner-authored — a
|
|
593
|
+
* delegated `read-own` is close to `read-any`. Grant an app the types it
|
|
594
|
+
* needs, not the suffix that looks narrowest. See
|
|
595
|
+
* docs/spec/access-control.md § Delegation: principal and subject.
|
|
596
|
+
*
|
|
597
|
+
* The grantee lives in content.granteeEntityId,
|
|
598
|
+
* not record.entityId. See docs/spec/access-control.md § Type-level
|
|
599
|
+
* grants.
|
|
255
600
|
*/
|
|
256
|
-
grant(entityId:
|
|
601
|
+
grant(entityId: EntityId | null, grants: Array<{
|
|
257
602
|
actions: GrantAction[];
|
|
258
603
|
typeId: TypeId;
|
|
259
604
|
}>): Promise<StackRecord[]>;
|
|
605
|
+
/**
|
|
606
|
+
* List _grant records. Omit `entityId` for all grants; pass null for
|
|
607
|
+
* only default grants; pass a specific entityId for the grants that
|
|
608
|
+
* currently apply to that entity (ones naming them, plus every default
|
|
609
|
+
* grant) — the same resolution hasGrant() uses.
|
|
610
|
+
*/
|
|
611
|
+
listGrants(entityId?: EntityId | null): Promise<StackRecord[]>;
|
|
612
|
+
/**
|
|
613
|
+
* The inverse of grant(): soft-deletes _grant records matching `entityId`
|
|
614
|
+
* (null for default grants) and each `{ typeId, actions }` pair, at the
|
|
615
|
+
* same granularity grant() writes. A soft delete like any other — the
|
|
616
|
+
* owner can undelete a revocation.
|
|
617
|
+
*/
|
|
618
|
+
revoke(entityId: EntityId | null, grants: Array<{
|
|
619
|
+
actions: GrantAction[];
|
|
620
|
+
typeId: TypeId;
|
|
621
|
+
}>): Promise<void>;
|
|
622
|
+
/**
|
|
623
|
+
* Validates the whole grant batch before any record is created, so a bad
|
|
624
|
+
* entry fails clean rather than leaving a partial set written. Actions
|
|
625
|
+
* must be known GrantAction values, typeIds must be well-formed, and the
|
|
626
|
+
* _grant/_config families are refused — see docs/spec/access-control.md
|
|
627
|
+
* § Type-level grants.
|
|
628
|
+
*/
|
|
629
|
+
private checkGrantsValid;
|
|
260
630
|
private seedSystemTypes;
|
|
261
|
-
|
|
631
|
+
/**
|
|
632
|
+
* Fast-fail for the ifVersion precondition using the already-fetched
|
|
633
|
+
* record. The adapter re-checks atomically at write time (the source of
|
|
634
|
+
* truth for concurrent writers) — this just skips validation and
|
|
635
|
+
* snapshotting work when the mismatch is already visible.
|
|
636
|
+
*/
|
|
637
|
+
private checkIfVersion;
|
|
638
|
+
/**
|
|
639
|
+
* Snapshot of a record's prior state, passed with the mutating adapter
|
|
640
|
+
* call so snapshot and mutation land in one atomic write. `associations`
|
|
641
|
+
* is always present ([] when empty) so restore can distinguish "cleared"
|
|
642
|
+
* from a snapshot that omits the key entirely ("leave as-is"). See
|
|
643
|
+
* docs/spec/versioning.md § Version history.
|
|
644
|
+
*/
|
|
645
|
+
private buildVersionSnapshot;
|
|
262
646
|
}
|
|
263
647
|
/**
|
|
264
|
-
* A permission-enforcing view of a Stack for a single
|
|
265
|
-
*
|
|
648
|
+
* A permission-enforcing view of a Stack for a single (principal, subject)
|
|
649
|
+
* pair, obtained
|
|
650
|
+
* via `stack.asEntity(entityId)`. Missing records throw StackNotFoundError
|
|
651
|
+
* (or return null on reads); existing-but-inaccessible ones throw
|
|
652
|
+
* StackPermissionError. See docs/spec/access-control.md.
|
|
266
653
|
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
654
|
+
* Two identities, one rule: **the principal governs authority, the subject
|
|
655
|
+
* governs attribution.** Grant lookup and the privilege-bearing gates that
|
|
656
|
+
* no grant reaches (setPermissions, group management, hard delete, widening
|
|
657
|
+
* access at create time) key on `principalEntityId`; authorship, `-own`
|
|
658
|
+
* matching, record-level permission resolution, and "files I uploaded"
|
|
659
|
+
* lookups key on `subjectEntityId`.
|
|
660
|
+
*
|
|
661
|
+
* Unconditional owner access follows that same split rather than one
|
|
662
|
+
* identity: it answers *what data is reachable* for the subject (an owner
|
|
663
|
+
* subject resolves past every permission check) and *who may exercise a
|
|
664
|
+
* privileged verb* for the principal (an owner app is not bounded by
|
|
665
|
+
* grants). Under delegation both halves apply, and a mistake on the
|
|
666
|
+
* authority side is an escalation rather than a preference.
|
|
272
667
|
*/
|
|
273
668
|
export declare class ScopedStack implements StackClient {
|
|
274
669
|
private readonly stack;
|
|
275
|
-
private readonly
|
|
276
|
-
|
|
670
|
+
private readonly principalEntityId;
|
|
671
|
+
private readonly subjectEntityId;
|
|
672
|
+
private readonly idTimestampSkewMs;
|
|
673
|
+
private readonly adapter;
|
|
674
|
+
constructor(stack: Stack, principalEntityId: EntityId | null, subjectEntityId: EntityId | null, idTimestampSkewMs: number | null, adapter: StackAdapter);
|
|
277
675
|
get features(): StackFeatures;
|
|
278
676
|
private resolveRecord;
|
|
677
|
+
/** Whether a delegated app is acting for someone other than itself. */
|
|
678
|
+
private get delegated();
|
|
679
|
+
/**
|
|
680
|
+
* Whether unconditional owner authority applies — the owner acting as
|
|
681
|
+
* itself. The verbs that rest on it are irreversible or disclose the
|
|
682
|
+
* sharing graph, so delegation never carries one to a subject, whichever
|
|
683
|
+
* side the owner is on. See
|
|
684
|
+
* docs/spec/access-control.md § Delegation: principal and subject.
|
|
685
|
+
*/
|
|
686
|
+
private get ownerActingAlone();
|
|
279
687
|
private checkRead;
|
|
280
688
|
private checkWrite;
|
|
281
689
|
/**
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
690
|
+
* Whether `grantee` holds a _grant covering one of `actions` for the
|
|
691
|
+
* type's family (grants match by baseId, so a version bump never orphans
|
|
692
|
+
* one). -own actions additionally require record.entityId === grantee,
|
|
693
|
+
* unless `matchOwn` is false — on the principal side of a delegated
|
|
694
|
+
* request the suffix is read as the bare verb, since which records are
|
|
695
|
+
* reachable is the subject's business. `allowDefault` decides whether a
|
|
696
|
+
* grant naming nobody counts. Anonymous grantees always return false.
|
|
697
|
+
*
|
|
698
|
+
* Reached only through subjectAllows()/principalAllows(), which fix those
|
|
699
|
+
* two flags per side of the intersection. Call one of those instead.
|
|
700
|
+
* See docs/spec/access-control.md § Type-level grants.
|
|
286
701
|
*/
|
|
287
702
|
private hasGrant;
|
|
703
|
+
/**
|
|
704
|
+
* The principal half of a delegated request's authority: does the app
|
|
705
|
+
* hold any grant permitting these verbs on this type at all. Bounds what
|
|
706
|
+
* the subject's own authority can reach through it, so a powerful app
|
|
707
|
+
* can never lend its reach to a weaker subject — nor the reverse.
|
|
708
|
+
* Vacuously true when there's no delegation, where the principal and
|
|
709
|
+
* subject checks would be the same question asked twice.
|
|
710
|
+
*
|
|
711
|
+
* Default grants don't count here. "Any authenticated entity" is about
|
|
712
|
+
* people who turn up, not software the owner installed — an app reaches
|
|
713
|
+
* only the types named to it, which is the whole of what containment
|
|
714
|
+
* promises.
|
|
715
|
+
*/
|
|
716
|
+
private principalAllows;
|
|
717
|
+
/**
|
|
718
|
+
* The subject half: which records are reachable, answered with `-own`
|
|
719
|
+
* matching and default grants both in force — the ordinary reading of a
|
|
720
|
+
* grant, since the subject is the entity a grant is written about.
|
|
721
|
+
*
|
|
722
|
+
* Paired with principalAllows() so that the two halves of the
|
|
723
|
+
* intersection are the only callers of hasGrant(): its flags differ per
|
|
724
|
+
* side and mean nothing on their own, so no call site sets them by hand.
|
|
725
|
+
*/
|
|
726
|
+
private subjectAllows;
|
|
288
727
|
private canRead;
|
|
289
728
|
private checkCreateGrant;
|
|
290
|
-
/**
|
|
729
|
+
/**
|
|
730
|
+
* `_group` records are managed, not merely written: only the owner or an
|
|
731
|
+
* `admin` roster holder may mutate them — ordinary write permissions and
|
|
732
|
+
* grants don't apply. Asked of both identities under delegation, like
|
|
733
|
+
* setPermissions(). See docs/spec/identity.md § Group.
|
|
734
|
+
*/
|
|
735
|
+
private isGroupManager;
|
|
736
|
+
/** Whether one identity, on its own, manages `record` — see isGroupManager(). */
|
|
737
|
+
private managesGroup;
|
|
738
|
+
/**
|
|
739
|
+
* Fetch a record the subject can reach and the principal holds `update` on
|
|
740
|
+
* (via permissions or an update grant), or throw. `mutating: false` marks
|
|
741
|
+
* the history readers, which borrow this gate without changing anything —
|
|
742
|
+
* the one way through it a `_grant` Record stays open to.
|
|
743
|
+
*/
|
|
291
744
|
private requireUpdatable;
|
|
292
|
-
/**
|
|
745
|
+
/**
|
|
746
|
+
* Fetch a record the subject can reach and the principal holds `delete` on
|
|
747
|
+
* (via permissions or a delete grant), or throw.
|
|
748
|
+
*/
|
|
293
749
|
private requireDeletable;
|
|
294
750
|
/**
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
|
|
751
|
+
* Whether this request may reference `recordId` (as a parentId or
|
|
752
|
+
* relationship target). Missing and unreadable both return false —
|
|
753
|
+
* indistinguishable, so this can't probe for a record's existence.
|
|
754
|
+
*/
|
|
755
|
+
private canReadReferent;
|
|
756
|
+
/**
|
|
757
|
+
* Whether this request can read some record referencing `fileId` —
|
|
758
|
+
* shared by canAccessFile() and the non-owner _attachment@1 create()
|
|
759
|
+
* carve-out, which deliberately excludes the uploader clause. Walks
|
|
760
|
+
* every referencing record, short-circuiting on the first readable one.
|
|
761
|
+
* See docs/spec/attachments.md § Creating `_attachment@1` records directly.
|
|
762
|
+
*/
|
|
763
|
+
private hasReadableReference;
|
|
764
|
+
/**
|
|
765
|
+
* Whether this request may reference or download `fileId` — the dual of
|
|
766
|
+
* getAttachment()'s access rule. Nonexistent and inaccessible are
|
|
767
|
+
* indistinguishable (both false), so no confirmation oracle for guessed
|
|
768
|
+
* hashes. See docs/spec/access-control.md § Reference-creation gating.
|
|
769
|
+
*/
|
|
770
|
+
private canAccessFile;
|
|
771
|
+
/** Names of the type's top-level file-ref fields — the content-reference half of attachmentFileId matching. */
|
|
772
|
+
private fileRefFieldNames;
|
|
773
|
+
/**
|
|
774
|
+
* Gates file-ref content fields on file access, mirroring the
|
|
775
|
+
* attachment-association gate — a file-ref field conveys attachment
|
|
776
|
+
* access exactly like an `attachment` association. Only fields present
|
|
777
|
+
* in `content` are checked (update() is a merge patch; untouched fields
|
|
778
|
+
* carry no new reference).
|
|
779
|
+
*/
|
|
780
|
+
private requireFileRefAccess;
|
|
781
|
+
/**
|
|
782
|
+
* Reference-creation gate for one association: `attachment` requires
|
|
783
|
+
* file access, `relationship` read access to its target; `tag` is
|
|
784
|
+
* unchecked, and `_group` roster associations are gated by the stricter
|
|
785
|
+
* isGroupManager() instead. See docs/spec/access-control.md
|
|
786
|
+
* § Reference-creation gating.
|
|
787
|
+
*/
|
|
788
|
+
private requireAssociationAccess;
|
|
789
|
+
/**
|
|
790
|
+
* Create a record on behalf of the subject: create grant required,
|
|
791
|
+
* anonymous denied, entityId set to the subject, client IDs skew-checked,
|
|
792
|
+
* reference-creating options gated, and non-owner `_attachment@1`
|
|
793
|
+
* creation refused save one carve-out. A scoped create always stamps
|
|
794
|
+
* authorship — an absent entityId means an unscoped `Stack` wrote it.
|
|
795
|
+
* See docs/spec/access-control.md and docs/spec/attachments.md.
|
|
299
796
|
*/
|
|
300
797
|
create<T extends Record<string, unknown> = Record<string, unknown>>(typeId: TypeId, content: T, opts?: CreateRecordOptions): Promise<StackRecord & {
|
|
301
798
|
content: T;
|
|
302
799
|
}>;
|
|
800
|
+
/**
|
|
801
|
+
* Whether this request may decide who else reaches a record — the rule
|
|
802
|
+
* setPermissions() enforces, asked at create time too so the reach it
|
|
803
|
+
* withholds can't be taken one step earlier while authoring. A delegated
|
|
804
|
+
* app is denied it: widening access is the one thing containment most
|
|
805
|
+
* needs to hold. Refused rather than silently ignored, so an app never
|
|
806
|
+
* believes it published something it didn't. Not `ownerActingAlone`:
|
|
807
|
+
* the record is the subject's own, so an owner principal grants it no
|
|
808
|
+
* reach the subject lacks.
|
|
809
|
+
* See docs/spec/access-control.md § Delegation: principal and subject.
|
|
810
|
+
*/
|
|
811
|
+
private mayGrantAccess;
|
|
812
|
+
/**
|
|
813
|
+
* Whether one identity, on its own, may decide who else reaches `record`
|
|
814
|
+
* — the owner-or-creator rule setPermissions() enforces, asked of one
|
|
815
|
+
* side at a time. See
|
|
816
|
+
* docs/spec/access-control.md § Delegation: principal and subject.
|
|
817
|
+
*/
|
|
818
|
+
private mayReshare;
|
|
303
819
|
get(id: string, opts?: GetRecordOptions): Promise<StackRecord | null>;
|
|
304
820
|
/**
|
|
305
|
-
* Query records, filtered to
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
821
|
+
* Query records, filtered to those this request can read. Pages are
|
|
822
|
+
* filtered then refilled, so a page may slightly overshoot `limit` but
|
|
823
|
+
* never skips a record. `total` is always null (see QueryResult.total).
|
|
824
|
+
* Grants are prefetched once, cursor-walked to exhaustion.
|
|
825
|
+
*/
|
|
826
|
+
query(query?: StackQuery): Promise<QueryResult>;
|
|
827
|
+
update(id: string, content: Record<string, unknown | null>, opts?: IfVersionOptions): Promise<StackRecord>;
|
|
828
|
+
/**
|
|
829
|
+
* Naming the software behind a key is the trust decision the `_app`
|
|
830
|
+
* registry exists to record, so both halves of that binding — `did` and
|
|
831
|
+
* `appId` — belong to the owner alone. Same reasoning that makes `_app`
|
|
832
|
+
* ungrantable, applied to the fields a lookup reads. Registering a card
|
|
833
|
+
* is already owner-only; without this, record-level `write` shared on a
|
|
834
|
+
* card would be a second way in: a card carrying no DID yet could be
|
|
835
|
+
* pointed at a write-holder's own key while keeping the name the owner
|
|
836
|
+
* gave it, or relabelled to claim another app's `appId`. `name` and
|
|
837
|
+
* `version` stay writable — they are display, not lookup.
|
|
312
838
|
*
|
|
313
|
-
*
|
|
839
|
+
* Owner *acting alone*, in both directions: a delegated app never holds
|
|
840
|
+
* it, and an owner principal doesn't lend it to a subject holding
|
|
841
|
+
* record-level `write` on a card — which would reopen the same route
|
|
842
|
+
* from the other side.
|
|
314
843
|
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
844
|
+
* `_entity` deliberately does not get this rule: naming people is what a
|
|
845
|
+
* contacts app does, so its cards stay writable by grant. Uniqueness and
|
|
846
|
+
* immutability still bind them. See docs/spec/identity.md § DID bindings.
|
|
847
|
+
*/
|
|
848
|
+
private requireOwnerForAppIdentity;
|
|
849
|
+
/**
|
|
850
|
+
* A self-reported `appId` must agree with the `_app` card naming the
|
|
851
|
+
* principal's DID, where the owner registered one. `principalId` is
|
|
852
|
+
* verified, so letting the pair disagree would leave a verified principal
|
|
853
|
+
* claiming a name the owner gave different software — the cross-check the
|
|
854
|
+
* registry exists for, refused at the write instead of left to each reader.
|
|
855
|
+
* A principal with no card keeps `appId` as the bare self-report it is for
|
|
856
|
+
* every undelegated writer.
|
|
857
|
+
* See docs/spec/identity.md § Attribution and what can be trusted.
|
|
858
|
+
*/
|
|
859
|
+
private requireAppIdMatchesPrincipal;
|
|
860
|
+
/**
|
|
861
|
+
* The owner's own DID is the one `_entity` binding a grantee may not claim.
|
|
862
|
+
* `ownerProfile` adopts whichever card holds it, so a card minted by
|
|
863
|
+
* someone else becomes the stack's own profile, and uniqueness then makes
|
|
864
|
+
* that permanent. Every other DID stays open to a contacts app, which is
|
|
865
|
+
* the reach `_entity` is grantable for.
|
|
866
|
+
* See docs/spec/identity.md § DID bindings.
|
|
867
|
+
*/
|
|
868
|
+
private requireOwnerForOwnerDid;
|
|
869
|
+
/**
|
|
870
|
+
* A `_grant` Record *is* authority, so rewriting one is the escalation
|
|
871
|
+
* UNGRANTABLE_SYSTEM_TYPES refuses at evaluation, reached by editing an
|
|
872
|
+
* existing grant rather than minting a fresh one. `grant()` and `revoke()`
|
|
873
|
+
* live on `Stack`, never `StackClient`, so no scoped write is lost. Writes
|
|
874
|
+
* only: reading a grant Record and its history stays on the ordinary gate.
|
|
875
|
+
* See docs/spec/access-control.md § Type-level grants.
|
|
876
|
+
*/
|
|
877
|
+
private requireOwnerForGrantRecord;
|
|
878
|
+
associate(id: string, association: Association, opts?: IfVersionOptions): Promise<void>;
|
|
879
|
+
dissociate(id: string, association: Association, opts?: IfVersionOptions): Promise<void>;
|
|
880
|
+
setPermissions(id: string, permissions: Permission[], opts?: IfVersionOptions): Promise<void>;
|
|
881
|
+
/**
|
|
882
|
+
* Hard delete is owner-only: it is irreversible and destroys version
|
|
883
|
+
* history, so neither the write bit nor delete-own/delete-any grants
|
|
884
|
+
* reach it, and delegation doesn't carry it either. Everyone else is
|
|
885
|
+
* limited to soft delete.
|
|
317
886
|
*/
|
|
318
|
-
query(query?: StackQuery): Promise<QueryResult>;
|
|
319
|
-
update(id: string, content: Record<string, unknown | null>): Promise<StackRecord>;
|
|
320
|
-
associate(id: string, association: Association): Promise<void>;
|
|
321
|
-
dissociate(id: string, association: Association): Promise<void>;
|
|
322
|
-
setPermissions(id: string, permissions: Permission[]): Promise<void>;
|
|
323
887
|
delete(id: string, opts?: DeleteRecordOptions): Promise<void>;
|
|
888
|
+
/**
|
|
889
|
+
* Reverse a soft delete. Gated the same as delete() — undelete is the
|
|
890
|
+
* inverse of soft delete, so granting one direction without the other
|
|
891
|
+
* would be backwards. Idempotent, per Stack.undelete().
|
|
892
|
+
*/
|
|
893
|
+
undelete(id: string, opts?: IfVersionOptions): Promise<StackRecord>;
|
|
894
|
+
/**
|
|
895
|
+
* History is the mutation/recovery surface, not a read surface — gated
|
|
896
|
+
* like update(), with snapshot `permissions` stripped for everyone but
|
|
897
|
+
* the owner acting alone — a snapshot's permissions are the stack's
|
|
898
|
+
* sharing graph, which delegation is not a route to. Reading history
|
|
899
|
+
* changes nothing, so it is the one path the `_grant` write fence leaves
|
|
900
|
+
* alone: seeing how a Record you can already read got that way is not
|
|
901
|
+
* the escalation that fence exists to stop, and losing it would leave a
|
|
902
|
+
* write-holder unable to audit the Record they hold.
|
|
903
|
+
* See docs/spec/versioning.md § History access.
|
|
904
|
+
*/
|
|
324
905
|
getVersions(id: string): Promise<RecordVersion[]>;
|
|
906
|
+
/** See getVersions() — same mutate-surface gate, same permissions stripping. */
|
|
325
907
|
getVersion(id: string, version: number): Promise<RecordVersion | null>;
|
|
326
|
-
restoreVersion(id: string, version: number): Promise<StackRecord>;
|
|
327
908
|
/**
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
909
|
+
* Re-runs the reference-creation checks against the snapshot, so a
|
|
910
|
+
* restore can't re-convey access to a file or record the subject can no
|
|
911
|
+
* longer reach today. Only the owner acting alone is exempt: under
|
|
912
|
+
* delegation the checks resolve against the subject, which is whose reach
|
|
913
|
+
* the restore would widen. See docs/spec/versioning.md § Restore semantics.
|
|
331
914
|
*/
|
|
332
|
-
|
|
915
|
+
restoreVersion(id: string, version: number, opts?: IfVersionOptions): Promise<StackRecord>;
|
|
333
916
|
/**
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
* and
|
|
917
|
+
* Store bytes and create an _attachment@1 metadata record (create grant
|
|
918
|
+
* on `_attachment@1` required; anonymous denied), returning that record.
|
|
919
|
+
* Authorship and principal are stamped exactly as create() does.
|
|
920
|
+
*/
|
|
921
|
+
putAttachment(data: Uint8Array, mimeType: string, filename?: string, appId?: AppId): Promise<StackRecord & {
|
|
922
|
+
content: AttachmentContent;
|
|
923
|
+
}>;
|
|
924
|
+
/**
|
|
925
|
+
* Download attachment bytes. Accessible to the owner, a reader of any
|
|
926
|
+
* referencing record, or the uploader pre-association — the same
|
|
927
|
+
* predicate as the reference-creation gate (canAccessFile).
|
|
337
928
|
*/
|
|
338
929
|
getAttachment(fileId: string): Promise<Uint8Array>;
|
|
339
930
|
/**
|
|
@@ -341,5 +932,10 @@ export declare class ScopedStack implements StackClient {
|
|
|
341
932
|
* Delegates to Stack.deleteAttachment(), which enforces the "not referenced" check.
|
|
342
933
|
*/
|
|
343
934
|
deleteAttachment(fileId: string): Promise<void>;
|
|
935
|
+
/**
|
|
936
|
+
* Sweep for unreferenced attachment bytes and delete them. Only the stack
|
|
937
|
+
* owner may run this. Delegates to Stack.collectAttachmentGarbage().
|
|
938
|
+
*/
|
|
939
|
+
collectAttachmentGarbage(opts?: CollectAttachmentGarbageOptions): Promise<CollectAttachmentGarbageResult>;
|
|
344
940
|
}
|
|
345
941
|
//# sourceMappingURL=stack.d.ts.map
|