@haverstack/core 0.39.0 → 0.40.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 +79 -16
- package/dist/access.js +1 -1
- package/dist/access.js.map +1 -1
- package/dist/adapter-entry.d.ts +2 -1
- package/dist/adapter-entry.d.ts.map +1 -1
- package/dist/adapter-entry.js +2 -1
- package/dist/adapter-entry.js.map +1 -1
- package/dist/changes.js +3 -3
- package/dist/combine.d.ts.map +1 -1
- package/dist/combine.js +2 -3
- package/dist/combine.js.map +1 -1
- package/dist/errors.d.ts +16 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +23 -2
- package/dist/errors.js.map +1 -1
- package/dist/grants.d.ts +7 -0
- package/dist/grants.d.ts.map +1 -1
- package/dist/grants.js +31 -3
- package/dist/grants.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/query-validation.d.ts +34 -1
- package/dist/query-validation.d.ts.map +1 -1
- package/dist/query-validation.js +101 -1
- package/dist/query-validation.js.map +1 -1
- package/dist/record-changes.d.ts +15 -1
- package/dist/record-changes.d.ts.map +1 -1
- package/dist/record-changes.js +33 -3
- package/dist/record-changes.js.map +1 -1
- package/dist/record-id.js +1 -1
- package/dist/record-id.js.map +1 -1
- package/dist/schema.d.ts +6 -7
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +33 -10
- package/dist/schema.js.map +1 -1
- package/dist/scoped-stack.d.ts +27 -17
- package/dist/scoped-stack.d.ts.map +1 -1
- package/dist/scoped-stack.js +111 -120
- package/dist/scoped-stack.js.map +1 -1
- package/dist/stack.d.ts +120 -44
- package/dist/stack.d.ts.map +1 -1
- package/dist/stack.js +153 -189
- package/dist/stack.js.map +1 -1
- package/dist/testing.d.ts +28 -11
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +42 -37
- package/dist/testing.js.map +1 -1
- package/dist/type-handle.d.ts +157 -0
- package/dist/type-handle.d.ts.map +1 -0
- package/dist/type-handle.js +131 -0
- package/dist/type-handle.js.map +1 -0
- package/dist/types.d.ts +63 -20
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/validate.d.ts +7 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +122 -7
- package/dist/validate.js.map +1 -1
- package/dist/wire-body.d.ts +9 -1
- package/dist/wire-body.d.ts.map +1 -1
- package/dist/wire-body.js +39 -0
- package/dist/wire-body.js.map +1 -1
- package/dist/wire-entry.d.ts +2 -2
- package/dist/wire-entry.d.ts.map +1 -1
- package/dist/wire-entry.js +4 -4
- package/dist/wire-entry.js.map +1 -1
- package/dist/wire-request.d.ts +18 -1
- package/dist/wire-request.d.ts.map +1 -1
- package/dist/wire-request.js +33 -1
- package/dist/wire-request.js.map +1 -1
- package/package.json +1 -1
package/dist/scoped-stack.d.ts
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* See docs/spec/access-control.md and docs/spec/events.md § Permission scoping.
|
|
19
19
|
*/
|
|
20
20
|
import { ChangeEmitter } from './changes.js';
|
|
21
|
-
import type { PutAttachmentOptions, AttachmentContent, AuthorityAssociation, DataAssociation, EntityId, FileId, QueryResult, RecordChange, RecordId, RecordVersion, RecordJournalEntry, JournalQuery, StackAdapter, StackCapabilities, IfVersionOptions, StackQuery, StackRecord, SubscribeOptions, TypeId, Unsubscribe, RecordChangeSet } from './types.js';
|
|
21
|
+
import type { PutAttachmentOptions, AssociationEdit, AttachmentContent, AuthorityAssociation, DataAssociation, EntityId, FileId, QueryResult, RecordChange, RecordId, RecordVersion, RecordJournalEntry, JournalQuery, VersionsQuery, StackAdapter, StackCapabilities, IfVersionOptions, StackQuery, StackRecord, SubscribeOptions, TypeId, Unsubscribe, RecordChangeSet } from './types.js';
|
|
22
|
+
import type { ContentOf, PatchOf, ReadonlyTypeSchema, TypedChange, TypedChangeSet, TypedQuery, TypedSubscribeOptions, TypedRecord, TypeHandle } from './type-handle.js';
|
|
22
23
|
import type { Stack, BackdatableCreateRecordOptions, CollectAttachmentGarbageOptions, CollectAttachmentGarbageResult, DeleteAndReturnResult, DeleteRecordOptions, DeleteResult, GetRecordOptions, StackClient } from './stack.js';
|
|
23
24
|
/**
|
|
24
25
|
* Held by Stack alone: the index re-exports ScopedStack but not this, so
|
|
@@ -189,14 +190,6 @@ export declare class ScopedStack implements StackClient {
|
|
|
189
190
|
* See docs/spec/access-control.md § Delegation: principal and subject.
|
|
190
191
|
*/
|
|
191
192
|
private requireVerb;
|
|
192
|
-
/**
|
|
193
|
-
* Refuse a mutation aimed at a soft-deleted Record. Asked only of a
|
|
194
|
-
* mutating caller, so the history readers borrowing this gate still work,
|
|
195
|
-
* and only after the authority decision, so "exists but deleted" is never
|
|
196
|
-
* a probe a stranger can run. See docs/spec/versioning.md § Mutations are
|
|
197
|
-
* refused, not applied to a tombstone.
|
|
198
|
-
*/
|
|
199
|
-
private refuseIfDeleted;
|
|
200
193
|
/**
|
|
201
194
|
* Fetch a record the subject can reach and the principal holds `delete` on
|
|
202
195
|
* (via permissions or a delete grant), or throw.
|
|
@@ -264,6 +257,7 @@ export declare class ScopedStack implements StackClient {
|
|
|
264
257
|
* something it didn't. See docs/spec/access-control.md and
|
|
265
258
|
* docs/spec/data-model.md § Record IDs.
|
|
266
259
|
*/
|
|
260
|
+
create<S extends ReadonlyTypeSchema>(handle: TypeHandle<S>, content: ContentOf<S>, opts?: BackdatableCreateRecordOptions): Promise<TypedRecord<S>>;
|
|
267
261
|
create<T extends Record<string, unknown> = Record<string, unknown>>(typeId: TypeId, content: T, opts?: BackdatableCreateRecordOptions): Promise<StackRecord & {
|
|
268
262
|
content: T;
|
|
269
263
|
}>;
|
|
@@ -292,6 +286,7 @@ export declare class ScopedStack implements StackClient {
|
|
|
292
286
|
* one does, so a caller learns only what it may read.
|
|
293
287
|
* See docs/spec/disclosure.md § Which refusal a Record answers with.
|
|
294
288
|
*/
|
|
289
|
+
get<S extends ReadonlyTypeSchema>(handle: TypeHandle<S>, id: RecordId): Promise<TypedRecord<S> | null>;
|
|
295
290
|
get(id: RecordId, opts?: GetRecordOptions): Promise<StackRecord | null>;
|
|
296
291
|
/**
|
|
297
292
|
* Runs through this.query(), so canRead() still applies — this answers a
|
|
@@ -307,6 +302,10 @@ export declare class ScopedStack implements StackClient {
|
|
|
307
302
|
* never skips a record. Grants are prefetched once, cursor-walked to
|
|
308
303
|
* exhaustion.
|
|
309
304
|
*/
|
|
305
|
+
query<S extends ReadonlyTypeSchema>(handle: TypeHandle<S>, query?: TypedQuery): Promise<{
|
|
306
|
+
records: TypedRecord<S>[];
|
|
307
|
+
cursor: string | null;
|
|
308
|
+
}>;
|
|
310
309
|
query(query?: StackQuery): Promise<QueryResult>;
|
|
311
310
|
/**
|
|
312
311
|
* Apply a change set on behalf of the subject. Authority is resolved
|
|
@@ -318,7 +317,9 @@ export declare class ScopedStack implements StackClient {
|
|
|
318
317
|
* applied and no key is silently dropped.
|
|
319
318
|
* See docs/spec/access-control.md § Composing a change set.
|
|
320
319
|
*/
|
|
320
|
+
mutate<S extends ReadonlyTypeSchema>(handle: TypeHandle<S>, id: RecordId, changes: TypedChangeSet<S>, opts?: IfVersionOptions): Promise<TypedRecord<S>>;
|
|
321
321
|
mutate(id: RecordId, changes: RecordChangeSet, opts?: IfVersionOptions): Promise<StackRecord>;
|
|
322
|
+
patchContent<S extends ReadonlyTypeSchema>(handle: TypeHandle<S>, id: RecordId, patch: PatchOf<S>, opts?: IfVersionOptions): Promise<TypedRecord<S>>;
|
|
322
323
|
patchContent(id: RecordId, patch: Record<string, unknown | null>, opts?: IfVersionOptions): Promise<StackRecord>;
|
|
323
324
|
/**
|
|
324
325
|
* The record, having established that this request may decide who else
|
|
@@ -393,14 +394,20 @@ export declare class ScopedStack implements StackClient {
|
|
|
393
394
|
*/
|
|
394
395
|
private requireOwnerForGrantRecord;
|
|
395
396
|
/**
|
|
396
|
-
* Gated on the write bit alone, which is why the
|
|
397
|
-
* first: authority reached through this verb would be exactly the
|
|
397
|
+
* Gated on the write bit alone, which is why the list is held to its
|
|
398
|
+
* rules first: authority reached through this verb would be exactly the
|
|
398
399
|
* escalation the partition exists to stop, decided before any record is
|
|
399
400
|
* read so it cannot depend on who is asking.
|
|
400
401
|
*/
|
|
401
|
-
associate(id: RecordId,
|
|
402
|
+
associate(id: RecordId, associations: DataAssociation[]): Promise<StackRecord>;
|
|
402
403
|
/** See associate() — the same write gate, the same kind refusal. */
|
|
403
|
-
dissociate(id: RecordId,
|
|
404
|
+
dissociate(id: RecordId, associations: DataAssociation[]): Promise<StackRecord>;
|
|
405
|
+
/**
|
|
406
|
+
* The write gate, plus the per-element reference gate on each add: what a
|
|
407
|
+
* remove names is already on the record, so it is gated by the write bit
|
|
408
|
+
* alone. See associate().
|
|
409
|
+
*/
|
|
410
|
+
amendAssociations(id: RecordId, changes: AssociationEdit[], surface?: string): Promise<StackRecord>;
|
|
404
411
|
/**
|
|
405
412
|
* Extend who reaches a record — the reshare gate's own verb, on the
|
|
406
413
|
* owner-or-creator rule the `permissions` key carries. The write bit
|
|
@@ -409,9 +416,11 @@ export declare class ScopedStack implements StackClient {
|
|
|
409
416
|
* scoping access is for.
|
|
410
417
|
* See docs/spec/access-control.md § Record-level permissions.
|
|
411
418
|
*/
|
|
412
|
-
grantAccess(id: RecordId,
|
|
413
|
-
/** Withdraw
|
|
414
|
-
revokeAccess(id: RecordId,
|
|
419
|
+
grantAccess(id: RecordId, permissions: AuthorityAssociation[]): Promise<StackRecord>;
|
|
420
|
+
/** Withdraw elements of who reaches a record — see grantAccess(). */
|
|
421
|
+
revokeAccess(id: RecordId, permissions: AuthorityAssociation[]): Promise<StackRecord>;
|
|
422
|
+
/** The reshare gate over an edit list — see grantAccess(). */
|
|
423
|
+
amendAccess(id: RecordId, changes: AssociationEdit[], surface?: string): Promise<StackRecord>;
|
|
415
424
|
/**
|
|
416
425
|
* Purge is owner-only: it is irreversible and destroys version
|
|
417
426
|
* history, so neither the write bit nor delete-own/delete-any grants
|
|
@@ -440,7 +449,7 @@ export declare class ScopedStack implements StackClient {
|
|
|
440
449
|
* already read got that way is not the escalation that fence exists to
|
|
441
450
|
* stop. See docs/spec/versioning.md § History access.
|
|
442
451
|
*/
|
|
443
|
-
getVersions(id: RecordId): Promise<RecordVersion[]>;
|
|
452
|
+
getVersions(id: RecordId, query?: VersionsQuery): Promise<RecordVersion[]>;
|
|
444
453
|
/**
|
|
445
454
|
* See getVersions() — the same mutate-surface gate, for the same reason.
|
|
446
455
|
*
|
|
@@ -510,6 +519,7 @@ export declare class ScopedStack implements StackClient {
|
|
|
510
519
|
* disclosure, the same reasoning that keeps a count of the whole match
|
|
511
520
|
* off a query result. See docs/spec/events.md § Permission scoping.
|
|
512
521
|
*/
|
|
522
|
+
subscribe<S extends ReadonlyTypeSchema>(handle: TypeHandle<S>, handler: (change: TypedChange<S>) => void, opts?: TypedSubscribeOptions): Promise<Unsubscribe>;
|
|
513
523
|
subscribe(handler: (change: RecordChange) => void, opts?: SubscribeOptions): Promise<Unsubscribe>;
|
|
514
524
|
/**
|
|
515
525
|
* canRead for one event, through the subscription's own cache. Grants
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scoped-stack.d.ts","sourceRoot":"","sources":["../src/scoped-stack.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAKH,OAAO,EACL,aAAa,EAKd,MAAM,cAAc,CAAC;AAGtB,OAAO,KAAK,EAKV,oBAAoB,EAEpB,iBAAiB,EACjB,oBAAoB,EACpB,eAAe,EACf,QAAQ,EACR,MAAM,EAIN,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,gBAAgB,EAChB,MAAM,EACN,WAAW,EACX,eAAe,EAChB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"scoped-stack.d.ts","sourceRoot":"","sources":["../src/scoped-stack.ts"],"names":[],"mappings":"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"}
|
package/dist/scoped-stack.js
CHANGED
|
@@ -22,14 +22,18 @@ import { validatePatchValues } from './validate.js';
|
|
|
22
22
|
import { checkAccess, groupRoleFromAssociations, isOwnerActingAlone } from './access.js';
|
|
23
23
|
import { ChangeEmitter, Subscription, matchesFilter, passesUnlistedBoundary, assertSinceUsable, } from './changes.js';
|
|
24
24
|
import { SYSTEM_TYPES } from './types.js';
|
|
25
|
-
import {
|
|
26
|
-
import { assertAuthorityAssociations, assertDataAssociations, assertSortCapability, assertValidAssociationFilters, assertValidSort, filtersContent, } from './query-validation.js';
|
|
25
|
+
import { StackError, StackNotFoundError, StackPermissionError, RelayScopeError, StackValidationError, } from './errors.js';
|
|
26
|
+
import { assertAssociationEdits, assertAuthorityAssociations, assertDataAssociations, assertSortCapability, assertValidAssociationFilters, assertValidBaseIdFilter, assertValidSort, filtersContent, } from './query-validation.js';
|
|
27
27
|
import { grantConveys, grantCoversGrantee, grantReach, loadGrantRecords, UNGRANTABLE_SYSTEM_TYPES, } from './grants.js';
|
|
28
28
|
import { bindingFieldsOf } from './identity-bindings.js';
|
|
29
29
|
import { assertAttachmentSize } from './limits.js';
|
|
30
30
|
import { validateIdTimestampSkew, validateRecordId } from './record-id.js';
|
|
31
31
|
import { DEFAULT_QUERY_LIMIT, MAX_QUERY_LIMIT, findFirstMatch, lookupEntityByDid, queryAllPages, } from './stack-reads.js';
|
|
32
32
|
import { associationDelta, associationEqual, isGroupRecord, presentDeleted, withoutAuthorityChanges, assertNonEmptyChangeSet, } from './record-changes.js';
|
|
33
|
+
// Every import from stack.js is type-only: a ScopedStack never constructs
|
|
34
|
+
// a Stack, so nothing here closes a runtime cycle with stack.ts, which does
|
|
35
|
+
// construct a ScopedStack.
|
|
36
|
+
import { isTypeHandle, typedCreate, typedGet, typedMutate, typedQuery, typedSubscribe, } from './type-handle.js';
|
|
33
37
|
/**
|
|
34
38
|
* The authority lookups canRead needs, held for the life of one
|
|
35
39
|
* subscription. A subscription is long-lived where a query is not, so the
|
|
@@ -196,7 +200,7 @@ export class ScopedStack {
|
|
|
196
200
|
get capabilities() {
|
|
197
201
|
return this.stack.capabilities;
|
|
198
202
|
}
|
|
199
|
-
resolveRecord = (id) => this.stack.get(id);
|
|
203
|
+
resolveRecord = (id) => this.stack.get(id, { includeDeleted: true });
|
|
200
204
|
/** Every `_grant` Record — see loadGrantRecords(). */
|
|
201
205
|
loadGrants = () => loadGrantRecords((q) => this.stack.query(q));
|
|
202
206
|
/** Whether a delegated app is acting for someone other than itself. */
|
|
@@ -286,9 +290,8 @@ export class ScopedStack {
|
|
|
286
290
|
grantRecords = prefetchedGrants;
|
|
287
291
|
}
|
|
288
292
|
else {
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
// versions. Cursor-walked to see grants past page one.
|
|
293
|
+
// Cursor-walked to see grants past page one. A stored baseId is
|
|
294
|
+
// judged by grantReach(), which refuses a versioned one.
|
|
292
295
|
grantRecords = await this.loadGrants();
|
|
293
296
|
}
|
|
294
297
|
for (const r of grantRecords) {
|
|
@@ -424,13 +427,11 @@ export class ScopedStack {
|
|
|
424
427
|
* the one way through it a `_grant` Record stays open to.
|
|
425
428
|
*/
|
|
426
429
|
async requireUpdatable(id, opts = {}) {
|
|
427
|
-
const mutating = opts.mutating ?? true;
|
|
428
430
|
// The `_grant` write fence applies to mutating callers only: reading a
|
|
429
431
|
// grant Record's history is not the escalation that fence exists to stop.
|
|
430
|
-
|
|
431
|
-
fenceGrantRecord: mutating,
|
|
432
|
+
return this.requireVerb(id, ['update-own', 'update-any'], {
|
|
433
|
+
fenceGrantRecord: opts.mutating ?? true,
|
|
432
434
|
});
|
|
433
|
-
return this.refuseIfDeleted(record, mutating);
|
|
434
435
|
}
|
|
435
436
|
/**
|
|
436
437
|
* The write gate both requireUpdatable() and requireDeletable() are: the
|
|
@@ -440,33 +441,26 @@ export class ScopedStack {
|
|
|
440
441
|
* See docs/spec/access-control.md § Delegation: principal and subject.
|
|
441
442
|
*/
|
|
442
443
|
async requireVerb(id, actions, opts) {
|
|
443
|
-
const record = await this.stack.get(id);
|
|
444
|
+
const record = await this.stack.get(id, { includeDeleted: true });
|
|
444
445
|
if (!record)
|
|
445
446
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
446
447
|
if (opts.fenceGrantRecord)
|
|
447
448
|
await this.requireOwnerForGrantRecord(record);
|
|
448
449
|
if (isGroupRecord(record)) {
|
|
449
|
-
if (!this.isGroupManager(record))
|
|
450
|
-
throw await this.denialFor(record);
|
|
450
|
+
if (!this.isGroupManager(record)) {
|
|
451
|
+
throw await this.denialFor(record, `Cannot change group "${id}": only its admins can manage it`);
|
|
452
|
+
}
|
|
451
453
|
return record;
|
|
452
454
|
}
|
|
453
|
-
const
|
|
454
|
-
(await this.subjectAllows(record.typeId, actions, { record }))
|
|
455
|
-
|
|
456
|
-
if (!
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
* mutating caller, so the history readers borrowing this gate still work,
|
|
463
|
-
* and only after the authority decision, so "exists but deleted" is never
|
|
464
|
-
* a probe a stranger can run. See docs/spec/versioning.md § Mutations are
|
|
465
|
-
* refused, not applied to a tombstone.
|
|
466
|
-
*/
|
|
467
|
-
refuseIfDeleted(record, mutating) {
|
|
468
|
-
if (mutating && record.deletedAt) {
|
|
469
|
-
throw new StackConflictError(`Record "${record.id}" is soft-deleted; undelete it before mutating it.`);
|
|
455
|
+
const subjectReaches = (await this.checkWrite(record)) ||
|
|
456
|
+
(await this.subjectAllows(record.typeId, actions, { record }));
|
|
457
|
+
const principalHolds = await this.principalAllows(record.typeId, actions);
|
|
458
|
+
if (!subjectReaches || !principalHolds) {
|
|
459
|
+
const verb = actions[0].split('-')[0];
|
|
460
|
+
const what = `Cannot ${verb} "${id}" (${record.typeId})`;
|
|
461
|
+
throw await this.denialFor(record, subjectReaches
|
|
462
|
+
? `${what}: the app acting for this entity holds no ${verb} grant`
|
|
463
|
+
: `${what}: requires ${actions.join(' or ')}`);
|
|
470
464
|
}
|
|
471
465
|
return record;
|
|
472
466
|
}
|
|
@@ -492,7 +486,7 @@ export class ScopedStack {
|
|
|
492
486
|
async canReadReferent(recordId) {
|
|
493
487
|
if (this.ownerActingAlone)
|
|
494
488
|
return true;
|
|
495
|
-
const record = await this.stack.get(recordId);
|
|
489
|
+
const record = await this.stack.get(recordId, { includeDeleted: true });
|
|
496
490
|
if (!record)
|
|
497
491
|
return false;
|
|
498
492
|
return this.canRead(record);
|
|
@@ -582,8 +576,9 @@ export class ScopedStack {
|
|
|
582
576
|
const value = content[field];
|
|
583
577
|
if (typeof value !== 'string')
|
|
584
578
|
continue;
|
|
585
|
-
if (!(await this.canAccessFile(value)))
|
|
586
|
-
throw new StackPermissionError();
|
|
579
|
+
if (!(await this.canAccessFile(value))) {
|
|
580
|
+
throw new StackPermissionError(`Cannot reference file "${value}" in field "${field}": it does not exist or you cannot read it`);
|
|
581
|
+
}
|
|
587
582
|
}
|
|
588
583
|
}
|
|
589
584
|
/**
|
|
@@ -600,8 +595,9 @@ export class ScopedStack {
|
|
|
600
595
|
*/
|
|
601
596
|
async requireAssociationAccess(typeId, association) {
|
|
602
597
|
if (association.kind === 'attachment') {
|
|
603
|
-
if (!(await this.canAccessFile(association.fileId)))
|
|
604
|
-
throw new StackPermissionError();
|
|
598
|
+
if (!(await this.canAccessFile(association.fileId))) {
|
|
599
|
+
throw new StackPermissionError(`Cannot reference file "${association.fileId}": it does not exist or you cannot read it`);
|
|
600
|
+
}
|
|
605
601
|
}
|
|
606
602
|
else if (association.kind === 'relationship' && baseIdOf(typeId) !== SYSTEM_TYPES.GROUP) {
|
|
607
603
|
const { target } = association;
|
|
@@ -611,22 +607,16 @@ export class ScopedStack {
|
|
|
611
607
|
// leave one spelling of a local Record ungated.
|
|
612
608
|
if (target.kind !== 'record' || target.stackUrl)
|
|
613
609
|
return;
|
|
614
|
-
if (!(await this.canReadReferent(target.recordId)))
|
|
615
|
-
throw new StackPermissionError();
|
|
610
|
+
if (!(await this.canReadReferent(target.recordId))) {
|
|
611
|
+
throw new StackPermissionError(`Cannot reference record "${target.recordId}": it does not exist or you cannot read it`);
|
|
612
|
+
}
|
|
616
613
|
}
|
|
617
614
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
* authorship — an absent `createdBy` means an unscoped `Stack` wrote it.
|
|
624
|
-
* `createdAt`/`updatedAt` are refused to everyone but the owner acting
|
|
625
|
-
* alone, rather than dropped, so an app never believes it published
|
|
626
|
-
* something it didn't. See docs/spec/access-control.md and
|
|
627
|
-
* docs/spec/data-model.md § Record IDs.
|
|
628
|
-
*/
|
|
629
|
-
async create(typeId, content, opts = {}) {
|
|
615
|
+
async create(typeIdOrHandle, content, opts = {}) {
|
|
616
|
+
if (isTypeHandle(typeIdOrHandle)) {
|
|
617
|
+
return typedCreate(this, typeIdOrHandle, content, opts);
|
|
618
|
+
}
|
|
619
|
+
const typeId = typeIdOrHandle;
|
|
630
620
|
const principal = this.#principalId;
|
|
631
621
|
if (!principal)
|
|
632
622
|
throw new StackPermissionError('Anonymous requesters cannot create records');
|
|
@@ -648,7 +638,7 @@ export class ScopedStack {
|
|
|
648
638
|
if (!this.ownerActingAlone && baseIdOf(typeId) === SYSTEM_TYPES.ATTACHMENT) {
|
|
649
639
|
const fileId = content.fileId;
|
|
650
640
|
if (typeof fileId !== 'string' || !(await this.hasReadableReference(fileId))) {
|
|
651
|
-
throw new StackPermissionError();
|
|
641
|
+
throw new StackPermissionError(`Cannot reference file "${String(fileId)}": it does not exist or you cannot read it`);
|
|
652
642
|
}
|
|
653
643
|
}
|
|
654
644
|
if (opts.permissions?.length && !this.mayGrantAccess()) {
|
|
@@ -672,7 +662,7 @@ export class ScopedStack {
|
|
|
672
662
|
}
|
|
673
663
|
}
|
|
674
664
|
if (opts.parentId !== undefined && !(await this.canReadReferent(opts.parentId))) {
|
|
675
|
-
throw new StackPermissionError();
|
|
665
|
+
throw new StackPermissionError(`Cannot reference record "${opts.parentId}" as parent: it does not exist or you cannot read it`);
|
|
676
666
|
}
|
|
677
667
|
for (const assoc of opts.associations ?? []) {
|
|
678
668
|
await this.requireAssociationAccess(typeId, assoc);
|
|
@@ -708,13 +698,11 @@ export class ScopedStack {
|
|
|
708
698
|
return false;
|
|
709
699
|
return entityId === this.stack.ownerEntityId || entityId === record.createdBy?.subjectId;
|
|
710
700
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
*/
|
|
717
|
-
async get(id, opts = {}) {
|
|
701
|
+
async get(idOrHandle, idOrOpts = {}) {
|
|
702
|
+
if (isTypeHandle(idOrHandle))
|
|
703
|
+
return typedGet(this, idOrHandle, idOrOpts);
|
|
704
|
+
const id = idOrHandle;
|
|
705
|
+
const opts = idOrOpts;
|
|
718
706
|
const record = await this.stack.get(id, opts);
|
|
719
707
|
if (!record)
|
|
720
708
|
return null;
|
|
@@ -734,16 +722,14 @@ export class ScopedStack {
|
|
|
734
722
|
async getOwnerEntity() {
|
|
735
723
|
return this.getEntityByDid(this.stack.ownerEntityId);
|
|
736
724
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
* exhaustion.
|
|
742
|
-
*/
|
|
743
|
-
async query(query = {}) {
|
|
725
|
+
async query(first = {}, typedQueryArg) {
|
|
726
|
+
if (isTypeHandle(first))
|
|
727
|
+
return typedQuery(this, first, typedQueryArg);
|
|
728
|
+
const query = first;
|
|
744
729
|
assertValidSort(query.sort);
|
|
745
730
|
assertSortCapability(query.sort, this.stack.capabilities);
|
|
746
731
|
assertValidAssociationFilters(query.filter);
|
|
732
|
+
assertValidBaseIdFilter(query.filter);
|
|
747
733
|
if (query.filter?.includeUnlisted && !this.ownerActingAlone) {
|
|
748
734
|
throw new StackPermissionError('includeUnlisted is owner-only');
|
|
749
735
|
}
|
|
@@ -771,17 +757,13 @@ export class ScopedStack {
|
|
|
771
757
|
} while (records.length < limit && page.cursor && totalFetched < maxFetched);
|
|
772
758
|
return { records, cursor: page.cursor };
|
|
773
759
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
* applied and no key is silently dropped.
|
|
782
|
-
* See docs/spec/access-control.md § Composing a change set.
|
|
783
|
-
*/
|
|
784
|
-
async mutate(id, changes, opts = {}) {
|
|
760
|
+
async mutate(first, second, third, fourth = {}) {
|
|
761
|
+
if (isTypeHandle(first)) {
|
|
762
|
+
return typedMutate(this, first, second, third, fourth);
|
|
763
|
+
}
|
|
764
|
+
const id = first;
|
|
765
|
+
const changes = second;
|
|
766
|
+
const opts = (third ?? {});
|
|
785
767
|
// Ahead of every gate: a malformed change set is a validation error
|
|
786
768
|
// for every requester, rather than one for the owner and a permission
|
|
787
769
|
// refusal for everyone else.
|
|
@@ -856,12 +838,15 @@ export class ScopedStack {
|
|
|
856
838
|
await this.requireAssociationAccess(record.typeId, assoc);
|
|
857
839
|
}
|
|
858
840
|
if (changes.parentId != null && !(await this.canReadReferent(changes.parentId))) {
|
|
859
|
-
throw new StackPermissionError();
|
|
841
|
+
throw new StackPermissionError(`Cannot reference record "${changes.parentId}" as parent: it does not exist or you cannot read it`);
|
|
860
842
|
}
|
|
861
843
|
return this.stack.mutate(id, authorized, { ...opts, ...this.actor });
|
|
862
844
|
}
|
|
863
|
-
async patchContent(
|
|
864
|
-
|
|
845
|
+
async patchContent(first, second, third, fourth) {
|
|
846
|
+
if (isTypeHandle(first)) {
|
|
847
|
+
return this.mutate(first, second, { contentPatch: third }, fourth);
|
|
848
|
+
}
|
|
849
|
+
return this.mutate(first, { contentPatch: second }, third);
|
|
865
850
|
}
|
|
866
851
|
/**
|
|
867
852
|
* The record, having established that this request may decide who else
|
|
@@ -871,15 +856,12 @@ export class ScopedStack {
|
|
|
871
856
|
* deliberately does not confer.
|
|
872
857
|
*/
|
|
873
858
|
async requireReshareable(id) {
|
|
874
|
-
const record = await this.stack.get(id);
|
|
859
|
+
const record = await this.stack.get(id, { includeDeleted: true });
|
|
875
860
|
if (!record)
|
|
876
861
|
throw new StackNotFoundError(`Record not found: "${id}"`);
|
|
877
862
|
await this.requireOwnerForGrantRecord(record);
|
|
878
863
|
await this.requireReshareOf(record);
|
|
879
|
-
|
|
880
|
-
// soft-delete refusal has to be asked here too — after the authority
|
|
881
|
-
// decision above, for the reason refuseIfDeleted() gives.
|
|
882
|
-
return this.refuseIfDeleted(record, true);
|
|
864
|
+
return record;
|
|
883
865
|
}
|
|
884
866
|
/**
|
|
885
867
|
* Whether a change set's `permissions` key actually moves the ACL. Read
|
|
@@ -896,8 +878,9 @@ export class ScopedStack {
|
|
|
896
878
|
}
|
|
897
879
|
/** The reshare decision alone, for a record already read and write-gated. */
|
|
898
880
|
async requireReshareOf(record) {
|
|
899
|
-
if (!this.canReshare(record))
|
|
900
|
-
throw await this.denialFor(record);
|
|
881
|
+
if (!this.canReshare(record)) {
|
|
882
|
+
throw await this.denialFor(record, `Cannot change permissions on "${record.id}": only its author or the stack owner can reshare it`);
|
|
883
|
+
}
|
|
901
884
|
}
|
|
902
885
|
/**
|
|
903
886
|
* Whether this request may decide who else reaches `record` — the
|
|
@@ -993,22 +976,31 @@ export class ScopedStack {
|
|
|
993
976
|
throw await this.denialFor(record, 'Only the stack owner may write a _grant record');
|
|
994
977
|
}
|
|
995
978
|
/**
|
|
996
|
-
* Gated on the write bit alone, which is why the
|
|
997
|
-
* first: authority reached through this verb would be exactly the
|
|
979
|
+
* Gated on the write bit alone, which is why the list is held to its
|
|
980
|
+
* rules first: authority reached through this verb would be exactly the
|
|
998
981
|
* escalation the partition exists to stop, decided before any record is
|
|
999
982
|
* read so it cannot depend on who is asking.
|
|
1000
983
|
*/
|
|
1001
|
-
async associate(id,
|
|
1002
|
-
|
|
1003
|
-
const record = await this.requireUpdatable(id);
|
|
1004
|
-
await this.requireAssociationAccess(record.typeId, association);
|
|
1005
|
-
return this.stack.associate(id, association, this.actor);
|
|
984
|
+
async associate(id, associations) {
|
|
985
|
+
return this.amendAssociations(id, associations.map((association) => ({ op: 'add', association })), 'associate()');
|
|
1006
986
|
}
|
|
1007
987
|
/** See associate() — the same write gate, the same kind refusal. */
|
|
1008
|
-
async dissociate(id,
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
988
|
+
async dissociate(id, associations) {
|
|
989
|
+
return this.amendAssociations(id, associations.map((association) => ({ op: 'remove', association })), 'dissociate()');
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* The write gate, plus the per-element reference gate on each add: what a
|
|
993
|
+
* remove names is already on the record, so it is gated by the write bit
|
|
994
|
+
* alone. See associate().
|
|
995
|
+
*/
|
|
996
|
+
async amendAssociations(id, changes, surface = 'amendAssociations()') {
|
|
997
|
+
assertAssociationEdits(changes, surface, 'data');
|
|
998
|
+
const record = await this.requireUpdatable(id);
|
|
999
|
+
for (const change of changes) {
|
|
1000
|
+
if (change.op === 'add')
|
|
1001
|
+
await this.requireAssociationAccess(record.typeId, change.association);
|
|
1002
|
+
}
|
|
1003
|
+
return this.stack.amendAssociations(id, changes, this.actor, surface);
|
|
1012
1004
|
}
|
|
1013
1005
|
/**
|
|
1014
1006
|
* Extend who reaches a record — the reshare gate's own verb, on the
|
|
@@ -1018,16 +1010,18 @@ export class ScopedStack {
|
|
|
1018
1010
|
* scoping access is for.
|
|
1019
1011
|
* See docs/spec/access-control.md § Record-level permissions.
|
|
1020
1012
|
*/
|
|
1021
|
-
async grantAccess(id,
|
|
1022
|
-
|
|
1023
|
-
await this.requireReshareable(id);
|
|
1024
|
-
return this.stack.grantAccess(id, permission, this.actor);
|
|
1013
|
+
async grantAccess(id, permissions) {
|
|
1014
|
+
return this.amendAccess(id, permissions.map((association) => ({ op: 'add', association })), 'grantAccess()');
|
|
1025
1015
|
}
|
|
1026
|
-
/** Withdraw
|
|
1027
|
-
async revokeAccess(id,
|
|
1028
|
-
|
|
1016
|
+
/** Withdraw elements of who reaches a record — see grantAccess(). */
|
|
1017
|
+
async revokeAccess(id, permissions) {
|
|
1018
|
+
return this.amendAccess(id, permissions.map((association) => ({ op: 'remove', association })), 'revokeAccess()');
|
|
1019
|
+
}
|
|
1020
|
+
/** The reshare gate over an edit list — see grantAccess(). */
|
|
1021
|
+
async amendAccess(id, changes, surface = 'amendAccess()') {
|
|
1022
|
+
assertAssociationEdits(changes, surface, 'authority');
|
|
1029
1023
|
await this.requireReshareable(id);
|
|
1030
|
-
return this.stack.
|
|
1024
|
+
return this.stack.amendAccess(id, changes, this.actor, surface);
|
|
1031
1025
|
}
|
|
1032
1026
|
/**
|
|
1033
1027
|
* Purge is owner-only: it is irreversible and destroys version
|
|
@@ -1069,9 +1063,9 @@ export class ScopedStack {
|
|
|
1069
1063
|
* already read got that way is not the escalation that fence exists to
|
|
1070
1064
|
* stop. See docs/spec/versioning.md § History access.
|
|
1071
1065
|
*/
|
|
1072
|
-
async getVersions(id) {
|
|
1066
|
+
async getVersions(id, query = {}) {
|
|
1073
1067
|
await this.requireUpdatable(id, { mutating: false });
|
|
1074
|
-
return this.stack.getVersions(id);
|
|
1068
|
+
return this.stack.getVersions(id, query);
|
|
1075
1069
|
}
|
|
1076
1070
|
/**
|
|
1077
1071
|
* See getVersions() — the same mutate-surface gate, for the same reason.
|
|
@@ -1169,8 +1163,9 @@ export class ScopedStack {
|
|
|
1169
1163
|
* predicate as the reference-creation gate (canAccessFile).
|
|
1170
1164
|
*/
|
|
1171
1165
|
async getAttachment(fileId) {
|
|
1172
|
-
if (!(await this.canAccessFile(fileId)))
|
|
1173
|
-
throw new StackPermissionError();
|
|
1166
|
+
if (!(await this.canAccessFile(fileId))) {
|
|
1167
|
+
throw new StackPermissionError(`Cannot read file "${fileId}": it does not exist or you cannot read it`);
|
|
1168
|
+
}
|
|
1174
1169
|
return this.stack.getAttachment(fileId);
|
|
1175
1170
|
}
|
|
1176
1171
|
/**
|
|
@@ -1189,17 +1184,12 @@ export class ScopedStack {
|
|
|
1189
1184
|
this.requireOwnerActingAlone('Only the stack owner can collect attachment garbage');
|
|
1190
1185
|
return this.stack.collectAttachmentGarbage({ ...opts, ...this.actor });
|
|
1191
1186
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
* than an empty or redacted one: the existence of a change is itself a
|
|
1199
|
-
* disclosure, the same reasoning that keeps a count of the whole match
|
|
1200
|
-
* off a query result. See docs/spec/events.md § Permission scoping.
|
|
1201
|
-
*/
|
|
1202
|
-
async subscribe(handler, opts = {}) {
|
|
1187
|
+
async subscribe(first, second, third) {
|
|
1188
|
+
if (isTypeHandle(first)) {
|
|
1189
|
+
return typedSubscribe(this, first, second, third);
|
|
1190
|
+
}
|
|
1191
|
+
const handler = first;
|
|
1192
|
+
const opts = (second ?? {});
|
|
1203
1193
|
this.assertStackOpen();
|
|
1204
1194
|
if (this.relaysChanges) {
|
|
1205
1195
|
throw new RelayScopeError('This stack relays changes from elsewhere, and a scoped view cannot narrow that feed: ' +
|
|
@@ -1209,6 +1199,7 @@ export class ScopedStack {
|
|
|
1209
1199
|
// A relaying stack was already refused above, so relaysChanges is
|
|
1210
1200
|
// always false here — `since` never has a cursor to mean anything by.
|
|
1211
1201
|
assertSinceUsable(opts.since, false);
|
|
1202
|
+
assertValidBaseIdFilter(opts.filter);
|
|
1212
1203
|
if (opts.includeUnlisted && !this.ownerActingAlone) {
|
|
1213
1204
|
throw new StackPermissionError('includeUnlisted is owner-only');
|
|
1214
1205
|
}
|