@haverstack/core 0.40.0 → 0.42.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 +121 -59
- package/dist/access.d.ts +34 -36
- package/dist/access.d.ts.map +1 -1
- package/dist/access.js +65 -42
- package/dist/access.js.map +1 -1
- package/dist/{combine.d.ts → adapter/combine.d.ts} +8 -1
- package/dist/adapter/combine.d.ts.map +1 -0
- package/dist/{combine.js → adapter/combine.js} +7 -0
- package/dist/adapter/combine.js.map +1 -0
- package/dist/{adapter-entry.d.ts → adapter/index.d.ts} +6 -5
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/{adapter-entry.js → adapter/index.js} +5 -4
- package/dist/adapter/index.js.map +1 -0
- package/dist/{sort.d.ts → adapter/sort.d.ts} +20 -20
- package/dist/adapter/sort.d.ts.map +1 -0
- package/dist/{sort.js → adapter/sort.js} +37 -27
- package/dist/adapter/sort.js.map +1 -0
- package/dist/associations/identity.d.ts +93 -0
- package/dist/associations/identity.d.ts.map +1 -0
- package/dist/associations/identity.js +198 -0
- package/dist/associations/identity.js.map +1 -0
- package/dist/associations/validation.d.ts +90 -0
- package/dist/associations/validation.d.ts.map +1 -0
- package/dist/associations/validation.js +339 -0
- package/dist/associations/validation.js.map +1 -0
- package/dist/attachment-records.d.ts +48 -0
- package/dist/attachment-records.d.ts.map +1 -0
- package/dist/attachment-records.js +52 -0
- package/dist/attachment-records.js.map +1 -0
- package/dist/collection.d.ts +182 -0
- package/dist/collection.d.ts.map +1 -0
- package/dist/collection.js +407 -0
- package/dist/collection.js.map +1 -0
- package/dist/content-match.d.ts +25 -0
- package/dist/content-match.d.ts.map +1 -0
- package/dist/content-match.js +51 -0
- package/dist/content-match.js.map +1 -0
- package/dist/content-translation.d.ts +42 -0
- package/dist/content-translation.d.ts.map +1 -0
- package/dist/content-translation.js +108 -0
- package/dist/content-translation.js.map +1 -0
- package/dist/did/did.d.ts.map +1 -0
- package/dist/{did.js → did/did.js} +3 -4
- package/dist/did/did.js.map +1 -0
- package/dist/{did-entry.d.ts → did/index.d.ts} +1 -1
- package/dist/did/index.d.ts.map +1 -0
- package/dist/{did-entry.js → did/index.js} +3 -3
- package/dist/did/index.js.map +1 -0
- package/dist/errors.d.ts +36 -63
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +35 -58
- package/dist/errors.js.map +1 -1
- package/dist/grants.d.ts +20 -43
- package/dist/grants.d.ts.map +1 -1
- package/dist/grants.js +71 -134
- package/dist/grants.js.map +1 -1
- package/dist/id.d.ts +13 -31
- package/dist/id.d.ts.map +1 -1
- package/dist/id.js +13 -31
- package/dist/id.js.map +1 -1
- package/dist/index.d.ts +14 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +141 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +219 -0
- package/dist/install.js.map +1 -0
- package/dist/merge.d.ts.map +1 -1
- package/dist/merge.js +5 -11
- package/dist/merge.js.map +1 -1
- package/dist/migration-steps.d.ts +102 -0
- package/dist/migration-steps.d.ts.map +1 -0
- package/dist/migration-steps.js +293 -0
- package/dist/migration-steps.js.map +1 -0
- package/dist/query-validation.d.ts +28 -94
- package/dist/query-validation.d.ts.map +1 -1
- package/dist/query-validation.js +156 -336
- package/dist/query-validation.js.map +1 -1
- package/dist/record-changes.d.ts +2 -117
- package/dist/record-changes.d.ts.map +1 -1
- package/dist/record-changes.js +10 -212
- package/dist/record-changes.js.map +1 -1
- package/dist/schema.d.ts +18 -7
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +59 -30
- package/dist/schema.js.map +1 -1
- package/dist/scoped-stack/authority.d.ts +156 -0
- package/dist/scoped-stack/authority.d.ts.map +1 -0
- package/dist/scoped-stack/authority.js +384 -0
- package/dist/scoped-stack/authority.js.map +1 -0
- package/dist/scoped-stack/scoped-stack.d.ts +338 -0
- package/dist/scoped-stack/scoped-stack.d.ts.map +1 -0
- package/dist/scoped-stack/scoped-stack.js +776 -0
- package/dist/scoped-stack/scoped-stack.js.map +1 -0
- package/dist/scoped-stack/subscription.d.ts +55 -0
- package/dist/scoped-stack/subscription.d.ts.map +1 -0
- package/dist/scoped-stack/subscription.js +110 -0
- package/dist/scoped-stack/subscription.js.map +1 -0
- package/dist/stack/attachments.d.ts +24 -0
- package/dist/stack/attachments.d.ts.map +1 -0
- package/dist/stack/attachments.js +113 -0
- package/dist/stack/attachments.js.map +1 -0
- package/dist/{changes.d.ts → stack/changes.d.ts} +29 -65
- package/dist/stack/changes.d.ts.map +1 -0
- package/dist/{changes.js → stack/changes.js} +32 -66
- package/dist/stack/changes.js.map +1 -0
- package/dist/stack/client.d.ts +271 -0
- package/dist/stack/client.d.ts.map +1 -0
- package/dist/stack/client.js +9 -0
- package/dist/stack/client.js.map +1 -0
- package/dist/stack/identity-bindings.d.ts.map +1 -0
- package/dist/{identity-bindings.js → stack/identity-bindings.js} +8 -14
- package/dist/stack/identity-bindings.js.map +1 -0
- package/dist/stack/install-app.d.ts +22 -0
- package/dist/stack/install-app.d.ts.map +1 -0
- package/dist/stack/install-app.js +260 -0
- package/dist/stack/install-app.js.map +1 -0
- package/dist/stack/integrity.d.ts +94 -0
- package/dist/stack/integrity.d.ts.map +1 -0
- package/dist/stack/integrity.js +268 -0
- package/dist/stack/integrity.js.map +1 -0
- package/dist/{limits.d.ts → stack/limits.d.ts} +2 -5
- package/dist/stack/limits.d.ts.map +1 -0
- package/dist/{limits.js → stack/limits.js} +3 -6
- package/dist/stack/limits.js.map +1 -0
- package/dist/stack/migrations.d.ts +59 -0
- package/dist/stack/migrations.d.ts.map +1 -0
- package/dist/stack/migrations.js +141 -0
- package/dist/stack/migrations.js.map +1 -0
- package/dist/{stack-reads.d.ts → stack/reads.d.ts} +33 -6
- package/dist/stack/reads.d.ts.map +1 -0
- package/dist/{stack-reads.js → stack/reads.js} +49 -7
- package/dist/stack/reads.js.map +1 -0
- package/dist/stack/record-id.d.ts +35 -0
- package/dist/stack/record-id.d.ts.map +1 -0
- package/dist/stack/record-id.js +82 -0
- package/dist/stack/record-id.js.map +1 -0
- package/dist/stack/stack.d.ts +438 -0
- package/dist/stack/stack.d.ts.map +1 -0
- package/dist/stack/stack.js +1366 -0
- package/dist/stack/stack.js.map +1 -0
- package/dist/stack/system-types.d.ts +16 -0
- package/dist/stack/system-types.d.ts.map +1 -0
- package/dist/stack/system-types.js +111 -0
- package/dist/stack/system-types.js.map +1 -0
- package/dist/stack/type-grants.d.ts +20 -0
- package/dist/stack/type-grants.d.ts.map +1 -0
- package/dist/stack/type-grants.js +69 -0
- package/dist/stack/type-grants.js.map +1 -0
- package/dist/stack/write-expectation.d.ts +78 -0
- package/dist/stack/write-expectation.d.ts.map +1 -0
- package/dist/stack/write-expectation.js +71 -0
- package/dist/stack/write-expectation.js.map +1 -0
- package/dist/testing.d.ts +21 -26
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +50 -70
- package/dist/testing.js.map +1 -1
- package/dist/type-handle.d.ts +67 -78
- package/dist/type-handle.d.ts.map +1 -1
- package/dist/type-handle.js +44 -111
- package/dist/type-handle.js.map +1 -1
- package/dist/types/adapter.d.ts +223 -0
- package/dist/types/adapter.d.ts.map +1 -0
- package/dist/types/adapter.js +5 -0
- package/dist/types/adapter.js.map +1 -0
- package/dist/types/associations.d.ts +156 -0
- package/dist/types/associations.d.ts.map +1 -0
- package/dist/types/associations.js +13 -0
- package/dist/types/associations.js.map +1 -0
- package/dist/types/capabilities.d.ts +89 -0
- package/dist/types/capabilities.d.ts.map +1 -0
- package/dist/types/capabilities.js +5 -0
- package/dist/types/capabilities.js.map +1 -0
- package/dist/types/events.d.ts +155 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +5 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/ids.d.ts +43 -0
- package/dist/types/ids.d.ts.map +1 -0
- package/dist/types/ids.js +5 -0
- package/dist/types/ids.js.map +1 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +19 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/journal.d.ts +47 -0
- package/dist/types/journal.d.ts.map +1 -0
- package/dist/types/journal.js +5 -0
- package/dist/types/journal.js.map +1 -0
- package/dist/types/query.d.ts +185 -0
- package/dist/types/query.d.ts.map +1 -0
- package/dist/types/query.js +10 -0
- package/dist/types/query.js.map +1 -0
- package/dist/types/records.d.ts +76 -0
- package/dist/types/records.d.ts.map +1 -0
- package/dist/types/records.js +5 -0
- package/dist/types/records.js.map +1 -0
- package/dist/types/schema.d.ts +74 -0
- package/dist/types/schema.d.ts.map +1 -0
- package/dist/types/schema.js +5 -0
- package/dist/types/schema.js.map +1 -0
- package/dist/types/system.d.ts +161 -0
- package/dist/types/system.d.ts.map +1 -0
- package/dist/types/system.js +32 -0
- package/dist/types/system.js.map +1 -0
- package/dist/types/tokens.d.ts +39 -0
- package/dist/types/tokens.d.ts.map +1 -0
- package/dist/types/tokens.js +5 -0
- package/dist/types/tokens.js.map +1 -0
- package/dist/validate.d.ts +29 -70
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +65 -97
- package/dist/validate.js.map +1 -1
- package/dist/{attachment-download.d.ts → wire/attachment-download.d.ts} +0 -57
- package/dist/wire/attachment-download.d.ts.map +1 -0
- package/dist/{attachment-download.js → wire/attachment-download.js} +0 -61
- package/dist/wire/attachment-download.js.map +1 -0
- package/dist/{auth.d.ts → wire/auth.d.ts} +5 -8
- package/dist/wire/auth.d.ts.map +1 -0
- package/dist/{auth.js → wire/auth.js} +5 -8
- package/dist/wire/auth.js.map +1 -0
- package/dist/{wire-body.d.ts → wire/body.d.ts} +13 -5
- package/dist/wire/body.d.ts.map +1 -0
- package/dist/{wire-body.js → wire/body.js} +65 -7
- package/dist/wire/body.js.map +1 -0
- package/dist/{wire-entry.d.ts → wire/index.d.ts} +12 -12
- package/dist/wire/index.d.ts.map +1 -0
- package/dist/{wire-entry.js → wire/index.js} +13 -17
- package/dist/wire/index.js.map +1 -0
- package/dist/{wire-record.d.ts → wire/record.d.ts} +10 -22
- package/dist/wire/record.d.ts.map +1 -0
- package/dist/{wire-record.js → wire/record.js} +12 -24
- package/dist/wire/record.js.map +1 -0
- package/dist/{wire-request.d.ts → wire/request.d.ts} +20 -37
- package/dist/wire/request.d.ts.map +1 -0
- package/dist/{wire-request.js → wire/request.js} +79 -54
- package/dist/wire/request.js.map +1 -0
- package/package.json +7 -7
- package/dist/adapter-entry.d.ts.map +0 -1
- package/dist/adapter-entry.js.map +0 -1
- package/dist/attachment-download.d.ts.map +0 -1
- package/dist/attachment-download.js.map +0 -1
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/changes.d.ts.map +0 -1
- package/dist/changes.js.map +0 -1
- package/dist/combine.d.ts.map +0 -1
- package/dist/combine.js.map +0 -1
- package/dist/did-entry.d.ts.map +0 -1
- package/dist/did-entry.js.map +0 -1
- package/dist/did.d.ts.map +0 -1
- package/dist/did.js.map +0 -1
- package/dist/identity-bindings.d.ts.map +0 -1
- package/dist/identity-bindings.js.map +0 -1
- package/dist/limits.d.ts.map +0 -1
- package/dist/limits.js.map +0 -1
- package/dist/record-id.d.ts +0 -70
- package/dist/record-id.d.ts.map +0 -1
- package/dist/record-id.js +0 -127
- package/dist/record-id.js.map +0 -1
- package/dist/scoped-stack.d.ts +0 -532
- package/dist/scoped-stack.d.ts.map +0 -1
- package/dist/scoped-stack.js +0 -1219
- package/dist/scoped-stack.js.map +0 -1
- package/dist/sort.d.ts.map +0 -1
- package/dist/sort.js.map +0 -1
- package/dist/stack-reads.d.ts.map +0 -1
- package/dist/stack-reads.js.map +0 -1
- package/dist/stack.d.ts +0 -971
- package/dist/stack.d.ts.map +0 -1
- package/dist/stack.js +0 -2012
- package/dist/stack.js.map +0 -1
- package/dist/types.d.ts +0 -1250
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -49
- package/dist/types.js.map +0 -1
- package/dist/wire-body.d.ts.map +0 -1
- package/dist/wire-body.js.map +0 -1
- package/dist/wire-entry.d.ts.map +0 -1
- package/dist/wire-entry.js.map +0 -1
- package/dist/wire-record.d.ts.map +0 -1
- package/dist/wire-record.js.map +0 -1
- package/dist/wire-request.d.ts.map +0 -1
- package/dist/wire-request.js.map +0 -1
- /package/dist/{did.d.ts → did/did.d.ts} +0 -0
- /package/dist/{identity-bindings.d.ts → stack/identity-bindings.d.ts} +0 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App install verbs
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* Planning, applying and withdrawing an app's install over an unscoped
|
|
5
|
+
* Stack: the bodies of Stack.planInstall(), installApp() and
|
|
6
|
+
* uninstallApp(), whose doc comments describe the contract. Written
|
|
7
|
+
* against Stack's public API alone, so every write here passes the same
|
|
8
|
+
* checks any other caller's would. See docs/spec/apps.md.
|
|
9
|
+
*/
|
|
10
|
+
import { ARGUMENTS_INVALID, StackConflictError, StackNotFoundError, StackSchemaDriftError, StackValidationError, } from '../errors.js';
|
|
11
|
+
import { checkGrantValid } from '../grants.js';
|
|
12
|
+
import { INSTALL_APP_LABEL, INSTALL_GRANT_LABEL, claimedFamilies, familyStanding, grantIsRequest, installAppLink, installGrantLink, installReader, linkedIds, namespaceOf, ownTypeIds, planFingerprint, sameRequest, snapshotManifest, } from '../install.js';
|
|
13
|
+
import { baseIdOf, diffSchemas, hashSchema, parseTypeId } from '../schema.js';
|
|
14
|
+
import { findAppCardByDid, loadInstallRecords, queryAllPages } from './reads.js';
|
|
15
|
+
import { SYSTEM_TYPES } from '../types/index.js';
|
|
16
|
+
import { validateSchemaFieldNames, validateSchemaReservedNames, validateSchemaShape, } from '../validate.js';
|
|
17
|
+
export async function planInstall(stack, submitted, opts) {
|
|
18
|
+
const { did } = opts;
|
|
19
|
+
const manifest = snapshotManifest(submitted);
|
|
20
|
+
checkManifest(manifest, did);
|
|
21
|
+
const installs = await loadInstallRecords(stack);
|
|
22
|
+
const existing = installs.find((r) => r.content.appId === manifest.appId) ?? null;
|
|
23
|
+
const ownVersions = ownTypeIds(manifest);
|
|
24
|
+
const ownFamilies = new Set(ownVersions.map(baseIdOf));
|
|
25
|
+
const card = await findAppCardByDid(stack, did);
|
|
26
|
+
if (card && card.content.appId !== manifest.appId) {
|
|
27
|
+
throw new StackConflictError(`${did} is registered to "${card.content.appId}", not "${manifest.appId}"`);
|
|
28
|
+
}
|
|
29
|
+
const claimed = existing ? claimedFamilies(existing.content) : new Set();
|
|
30
|
+
const defined = new Set(existing?.content.defines ?? []);
|
|
31
|
+
const prior = existing?.content.requests ?? [];
|
|
32
|
+
const foreignRequests = [];
|
|
33
|
+
for (const r of manifest.requests) {
|
|
34
|
+
const standing = familyStanding(r.baseId, manifest.appId);
|
|
35
|
+
if (standing === 'own')
|
|
36
|
+
continue;
|
|
37
|
+
const owner = standing === 'foreign'
|
|
38
|
+
? namespaceOf(r.baseId)
|
|
39
|
+
: standing === 'commons'
|
|
40
|
+
? 'commons'
|
|
41
|
+
: 'system';
|
|
42
|
+
foreignRequests.push({ ...r, owner });
|
|
43
|
+
}
|
|
44
|
+
const typeChanges = [];
|
|
45
|
+
for (const t of manifest.types) {
|
|
46
|
+
const current = await stack.getType(t.id);
|
|
47
|
+
if (!current)
|
|
48
|
+
typeChanges.push({ id: t.id, change: 'new' });
|
|
49
|
+
else if (current.schemaHash !== (await hashSchema(t.schema))) {
|
|
50
|
+
// Refused here, not left to defineType(): installApp() defines types one
|
|
51
|
+
// by one, so a drift found there leaves the earlier ones written.
|
|
52
|
+
const violations = diffSchemas(current.schema, t.schema);
|
|
53
|
+
if (violations.length > 0)
|
|
54
|
+
throw new StackSchemaDriftError(t.id, violations);
|
|
55
|
+
typeChanges.push({ id: t.id, change: 'schema' });
|
|
56
|
+
}
|
|
57
|
+
else if (current.name !== t.name)
|
|
58
|
+
typeChanges.push({ id: t.id, change: 'name' });
|
|
59
|
+
}
|
|
60
|
+
const linkedKeys = [];
|
|
61
|
+
for (const id of existing ? linkedIds(existing, INSTALL_APP_LABEL) : []) {
|
|
62
|
+
const key = (await stack.get(id))?.content?.did;
|
|
63
|
+
if (typeof key === 'string')
|
|
64
|
+
linkedKeys.push(key);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
manifest,
|
|
68
|
+
did,
|
|
69
|
+
existing,
|
|
70
|
+
newFamilies: [...ownFamilies].filter((f) => !claimed.has(f)),
|
|
71
|
+
newVersions: ownVersions.filter((id) => !defined.has(id)),
|
|
72
|
+
requestsAdded: manifest.requests.filter((r) => !prior.some((p) => sameRequest(p, r))),
|
|
73
|
+
requestsRemoved: prior.filter((p) => !manifest.requests.some((r) => sameRequest(p, r))),
|
|
74
|
+
foreignRequests,
|
|
75
|
+
typeChanges,
|
|
76
|
+
newKey: !existing ||
|
|
77
|
+
!card ||
|
|
78
|
+
card.deletedAt !== undefined ||
|
|
79
|
+
!linkedIds(existing, INSTALL_APP_LABEL).includes(card.id),
|
|
80
|
+
linkedKeys,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export async function installApp(stack, plan) {
|
|
84
|
+
const fresh = await planInstall(stack, plan.manifest, { did: plan.did });
|
|
85
|
+
if (planFingerprint(fresh) !== planFingerprint(plan)) {
|
|
86
|
+
throw new StackConflictError(`The stack changed since the install of "${plan.manifest.appId}" was planned; plan it again`);
|
|
87
|
+
}
|
|
88
|
+
const { manifest, did, existing } = fresh;
|
|
89
|
+
for (const type of manifest.types)
|
|
90
|
+
await stack.defineType(type);
|
|
91
|
+
const card = await ensureAppCard(stack, manifest, did);
|
|
92
|
+
const defines = [...new Set([...(existing?.content.defines ?? []), ...ownTypeIds(manifest)])];
|
|
93
|
+
const requests = manifest.requests.map((r) => ({
|
|
94
|
+
baseId: r.baseId,
|
|
95
|
+
actions: [...r.actions],
|
|
96
|
+
}));
|
|
97
|
+
let install;
|
|
98
|
+
if (!existing) {
|
|
99
|
+
install = await stack.create(`${SYSTEM_TYPES.INSTALL}@1`, {
|
|
100
|
+
appId: manifest.appId,
|
|
101
|
+
name: manifest.name,
|
|
102
|
+
...(manifest.version !== undefined && { version: manifest.version }),
|
|
103
|
+
defines,
|
|
104
|
+
requests,
|
|
105
|
+
}, { associations: [installAppLink(card.id)] });
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const current = existing.deletedAt ? await stack.undelete(existing.id) : existing;
|
|
109
|
+
install = await stack.patchContent(existing.id, { name: manifest.name, version: manifest.version ?? null, defines, requests }, { ifVersion: current.version });
|
|
110
|
+
if (!linkedIds(install, INSTALL_APP_LABEL).includes(card.id)) {
|
|
111
|
+
install = await stack.associate(install.id, [installAppLink(card.id)]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return (await reconcileInstallGrants(stack, install));
|
|
115
|
+
}
|
|
116
|
+
export async function uninstallApp(stack, appId) {
|
|
117
|
+
const install = (await loadInstallRecords(stack)).find((r) => r.content.appId === appId && !r.deletedAt);
|
|
118
|
+
if (!install)
|
|
119
|
+
throw new StackNotFoundError(`No install for "${appId}"`);
|
|
120
|
+
const edits = [];
|
|
121
|
+
for (const id of linkedIds(install, INSTALL_GRANT_LABEL)) {
|
|
122
|
+
if (await stack.get(id))
|
|
123
|
+
await stack.delete(id);
|
|
124
|
+
edits.push({ op: 'remove', association: installGrantLink(id) });
|
|
125
|
+
}
|
|
126
|
+
if (edits.length > 0)
|
|
127
|
+
await stack.amendAssociations(install.id, edits);
|
|
128
|
+
const { record } = await stack.deleteAndReturn(install.id);
|
|
129
|
+
return record;
|
|
130
|
+
}
|
|
131
|
+
/** A manifest's own shape, and every request held to grantType()'s rules. */
|
|
132
|
+
function checkManifest(manifest, did) {
|
|
133
|
+
const errors = [];
|
|
134
|
+
if (typeof did !== 'string' || !did.startsWith('did:')) {
|
|
135
|
+
errors.push({ path: 'did', message: 'Expected a DID' });
|
|
136
|
+
}
|
|
137
|
+
if (typeof manifest.appId !== 'string' || manifest.appId === '') {
|
|
138
|
+
errors.push({ path: 'appId', message: 'Expected a non-empty appId' });
|
|
139
|
+
}
|
|
140
|
+
if (typeof manifest.name !== 'string' || manifest.name === '') {
|
|
141
|
+
errors.push({ path: 'name', message: 'Expected a non-empty name' });
|
|
142
|
+
}
|
|
143
|
+
const seen = new Set();
|
|
144
|
+
manifest.types.forEach((t, i) => {
|
|
145
|
+
if (seen.has(t.id)) {
|
|
146
|
+
errors.push({ path: `types[${i}].id`, message: `"${t.id}" is listed more than once` });
|
|
147
|
+
}
|
|
148
|
+
seen.add(t.id);
|
|
149
|
+
const schemaPath = `types[${i}].schema`;
|
|
150
|
+
const shapeErrors = validateSchemaShape(t.schema, schemaPath);
|
|
151
|
+
errors.push(...shapeErrors);
|
|
152
|
+
if (shapeErrors.length === 0) {
|
|
153
|
+
const schema = t.schema;
|
|
154
|
+
for (const e of validateSchemaReservedNames(schema)) {
|
|
155
|
+
errors.push({ ...e, path: `${schemaPath}.${e.path}` });
|
|
156
|
+
}
|
|
157
|
+
validateSchemaFieldNames(schema, schemaPath, errors);
|
|
158
|
+
}
|
|
159
|
+
const parsed = parseTypeId(t.id);
|
|
160
|
+
if (!parsed) {
|
|
161
|
+
errors.push({ path: `types[${i}].id`, message: 'Expected a versioned TypeId' });
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
const standing = familyStanding(parsed.baseId, manifest.appId);
|
|
165
|
+
if (standing === 'system' || standing === 'foreign') {
|
|
166
|
+
errors.push({
|
|
167
|
+
path: `types[${i}].id`,
|
|
168
|
+
message: standing === 'system'
|
|
169
|
+
? `"${parsed.baseId}" is a system type; no app can define it`
|
|
170
|
+
: `"${parsed.baseId}" is outside the namespace "${manifest.appId}"; request access to it instead of defining it`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
if (errors.length > 0)
|
|
176
|
+
throw new StackValidationError(errors, ARGUMENTS_INVALID);
|
|
177
|
+
for (const r of manifest.requests)
|
|
178
|
+
checkGrantValid(r.baseId, r.actions);
|
|
179
|
+
}
|
|
180
|
+
/** The key's `_app` card, created or undeleted as needed. */
|
|
181
|
+
async function ensureAppCard(stack, manifest, did) {
|
|
182
|
+
const card = await findAppCardByDid(stack, did);
|
|
183
|
+
if (!card) {
|
|
184
|
+
return stack.create(`${SYSTEM_TYPES.APP}@1`, {
|
|
185
|
+
appId: manifest.appId,
|
|
186
|
+
name: manifest.name,
|
|
187
|
+
...(manifest.version !== undefined && { version: manifest.version }),
|
|
188
|
+
did,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return card.deletedAt ? stack.undelete(card.id) : card;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Make the grants linked to `install` exactly its `requests`, once per
|
|
195
|
+
* live linked key: withdraw any that no longer match, write any missing,
|
|
196
|
+
* and drop links to grants that are gone.
|
|
197
|
+
*/
|
|
198
|
+
async function reconcileInstallGrants(stack, install) {
|
|
199
|
+
const { requests } = install.content;
|
|
200
|
+
const dids = [];
|
|
201
|
+
for (const id of linkedIds(install, INSTALL_APP_LABEL)) {
|
|
202
|
+
const did = (await stack.get(id))?.content?.did;
|
|
203
|
+
if (typeof did === 'string')
|
|
204
|
+
dids.push(did);
|
|
205
|
+
}
|
|
206
|
+
const edits = [];
|
|
207
|
+
const held = [];
|
|
208
|
+
for (const id of linkedIds(install, INSTALL_GRANT_LABEL)) {
|
|
209
|
+
const grant = await stack.get(id);
|
|
210
|
+
const content = grant?.content;
|
|
211
|
+
const wanted = content !== undefined &&
|
|
212
|
+
baseIdOf(grant.typeId) === SYSTEM_TYPES.GRANT &&
|
|
213
|
+
dids.some((did) => requests.some((r) => grantIsRequest(content, r, did)));
|
|
214
|
+
if (wanted) {
|
|
215
|
+
held.push(content);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (grant)
|
|
219
|
+
await stack.delete(id);
|
|
220
|
+
edits.push({ op: 'remove', association: installGrantLink(id) });
|
|
221
|
+
}
|
|
222
|
+
for (const did of dids) {
|
|
223
|
+
for (const r of requests) {
|
|
224
|
+
if (held.some((g) => grantIsRequest(g, r, did)))
|
|
225
|
+
continue;
|
|
226
|
+
const grant = await stack.grantType(r.baseId, {
|
|
227
|
+
actions: r.actions,
|
|
228
|
+
grantee: { kind: 'entity', entityId: did },
|
|
229
|
+
});
|
|
230
|
+
held.push(grant.content);
|
|
231
|
+
edits.push({ op: 'add', association: installGrantLink(grant.id) });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
let result = edits.length > 0 ? await stack.amendAssociations(install.id, edits) : install;
|
|
235
|
+
// Each linked key may read its own install, which is how an app learns
|
|
236
|
+
// what was approved; a key of this app no longer linked may not.
|
|
237
|
+
// See docs/spec/apps.md § Over the wire.
|
|
238
|
+
const appKeys = new Set((await queryAllPages((q) => stack.query(q), {
|
|
239
|
+
filter: { baseId: SYSTEM_TYPES.APP, includeDeleted: true, includeUnlisted: true },
|
|
240
|
+
}))
|
|
241
|
+
.map((r) => r.content)
|
|
242
|
+
.filter((c) => c.appId === install.content.appId)
|
|
243
|
+
.map((c) => c.did));
|
|
244
|
+
const readerOf = (p) => p.kind === 'permission' && p.label === 'read' && p.grantee.kind === 'entity'
|
|
245
|
+
? p.grantee.entityId
|
|
246
|
+
: null;
|
|
247
|
+
const current = (result.permissions ?? []).map(readerOf);
|
|
248
|
+
const access = [
|
|
249
|
+
...dids
|
|
250
|
+
.filter((did) => !current.includes(did))
|
|
251
|
+
.map((entityId) => ({ op: 'add', association: installReader(entityId) })),
|
|
252
|
+
...current
|
|
253
|
+
.filter((did) => did !== null && appKeys.has(did) && !dids.includes(did))
|
|
254
|
+
.map((entityId) => ({ op: 'remove', association: installReader(entityId) })),
|
|
255
|
+
];
|
|
256
|
+
if (access.length > 0)
|
|
257
|
+
result = await stack.amendAccess(install.id, access);
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=install-app.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-app.js","sourceRoot":"","sources":["../../src/stack/install-app.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write invariants
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* The integrity rules a Stack write owes whoever makes it — DID and appId
|
|
5
|
+
* bindings, containment, attachment metadata, `_config` ownership and a
|
|
6
|
+
* group's admin roster. They bind every requester, the stack owner
|
|
7
|
+
* included, which is why they live beneath `ScopedStack`'s permission
|
|
8
|
+
* gates rather than among them. Each reads unscoped: through the Stack it
|
|
9
|
+
* is handed, or a by-id `read` straight from its adapter.
|
|
10
|
+
*/
|
|
11
|
+
import type { RecordResolver } from '../access.js';
|
|
12
|
+
import type { Association, AttachmentContent, DataAssociation, EntityId, RecordId, StackRecord, TypeId } from '../types/index.js';
|
|
13
|
+
import type { Stack } from './stack.js';
|
|
14
|
+
/** Uniqueness for every unique binding field a newly created card claims. */
|
|
15
|
+
export declare function checkBindingsOnCreate(stack: Stack, typeId: TypeId, content: Record<string, unknown>): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Immutability for every binding field a patch touches, then uniqueness
|
|
18
|
+
* for the subset that carries it. Fields absent from the patch carry no
|
|
19
|
+
* new claim — a content patch is a merge, so an untouched binding is the one the
|
|
20
|
+
* card already holds.
|
|
21
|
+
*/
|
|
22
|
+
export declare function checkBindingsOnUpdate(stack: Stack, typeId: TypeId, id: RecordId, patch: Record<string, unknown | null>, existing: Record<string, unknown>, merged: Record<string, unknown>): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Bindings across a migration, whose content replaces the old wholesale.
|
|
25
|
+
* Immutability is asked over both families' binding fields, so a card can
|
|
26
|
+
* neither shed a DID by migrating out nor pick one up on the way in;
|
|
27
|
+
* uniqueness, of the destination only. See docs/spec/identity.md § DID bindings.
|
|
28
|
+
*/
|
|
29
|
+
export declare function checkBindingsOnMigrate(stack: Stack, fromTypeId: TypeId, toTypeId: TypeId, id: RecordId, existing: Record<string, unknown>, content: Record<string, unknown>): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* A binding is permanent once made: uniqueness stops a second card claiming
|
|
32
|
+
* a value, but only immutability stops an existing card being moved onto
|
|
33
|
+
* one. A subject whose key changes gets a new card.
|
|
34
|
+
* See docs/spec/identity.md § DID bindings.
|
|
35
|
+
*/
|
|
36
|
+
export declare function checkBindingImmutable(family: string, field: 'did' | 'appId', existing: unknown, next: unknown): void;
|
|
37
|
+
/**
|
|
38
|
+
* A caller-named `parentId` is a well-formed id of a record that exists —
|
|
39
|
+
* format first, so a malformed one is a 400 naming the problem. A restore
|
|
40
|
+
* names no destination and does not ask this.
|
|
41
|
+
* See docs/spec/data-model.md § Reparenting.
|
|
42
|
+
*/
|
|
43
|
+
export declare function assertParentExists(read: RecordResolver, id: string, parentId: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Refuse an edge that would make a record its own ancestor. Walks with an
|
|
46
|
+
* unscoped `read`, since links a requester cannot read could otherwise
|
|
47
|
+
* close a cycle. Read-then-write, so racing moves can both pass.
|
|
48
|
+
* See docs/spec/data-model.md § Reparenting.
|
|
49
|
+
*/
|
|
50
|
+
export declare function assertNoParentCycle(read: RecordResolver, id: string, parentId: string): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* The `_attachment@1` fields a write may not move, each with the message it
|
|
53
|
+
* is refused by. Ordered as reported. See docs/spec/attachments.md § The
|
|
54
|
+
* `_attachment` record type.
|
|
55
|
+
*/
|
|
56
|
+
declare const ATTACHMENT_IMMUTABLE_FIELDS: readonly [readonly ["mimeType", "mimeType is immutable after creation; delete and re-upload to change it"], readonly ["fileId", "fileId is immutable"], readonly ["size", "size is immutable"]];
|
|
57
|
+
type AttachmentImmutableField = (typeof ATTACHMENT_IMMUTABLE_FIELDS)[number][0];
|
|
58
|
+
/**
|
|
59
|
+
* mimeType belongs to the fileId: its first metadata record fixes it, and
|
|
60
|
+
* a conflicting later one is refused. Best-effort against a racing first
|
|
61
|
+
* upload; what survives the race is a deterministic winner. See
|
|
62
|
+
* docs/spec/attachments.md § The `_attachment` record type.
|
|
63
|
+
*/
|
|
64
|
+
export declare function checkAttachmentMimeTypeOnCreate(stack: Stack, content: AttachmentContent): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* An `attachmentRecordId` must name an `_attachment` record for the same
|
|
67
|
+
* `fileId`. Pointers already in `stored` are not re-asked, so restating an
|
|
68
|
+
* association never fails for a record deleted since.
|
|
69
|
+
* See docs/spec/attachments.md § Naming the upload a reference came from.
|
|
70
|
+
*/
|
|
71
|
+
export declare function checkAttachmentAssociationPointers(read: RecordResolver, associations: Association[] | undefined, stored?: Association[]): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Refuse a write that moves an `_attachment@1` record's fileId, size or
|
|
74
|
+
* mimeType. `violates` decides what counts as a move, since a patch names
|
|
75
|
+
* only what it changes while a migration re-sends all three.
|
|
76
|
+
* See docs/spec/attachments.md § The `_attachment` record type.
|
|
77
|
+
*/
|
|
78
|
+
export declare function assertAttachmentImmutable(violates: (field: AttachmentImmutableField) => boolean): void;
|
|
79
|
+
/**
|
|
80
|
+
* `_config.entityId` defines stack ownership; neither patchContent() nor
|
|
81
|
+
* restoreVersion() may change it. A conflict with stack integrity, not a
|
|
82
|
+
* schema violation — hence StackConflictError. See docs/spec.md § The
|
|
83
|
+
* `_config` record.
|
|
84
|
+
*/
|
|
85
|
+
export declare function checkConfigEntityIdUnchanged(existingEntityId: EntityId, newEntityId: EntityId): void;
|
|
86
|
+
/**
|
|
87
|
+
* Refuse a write that would leave a `_group` Record with no `admin`, asked
|
|
88
|
+
* of the roster the write would produce. An integrity rule, not a
|
|
89
|
+
* permission, so it binds the stack owner too.
|
|
90
|
+
* See docs/spec/identity.md § Group.
|
|
91
|
+
*/
|
|
92
|
+
export declare function assertGroupAdminRemains(record: StackRecord, next: DataAssociation[]): void;
|
|
93
|
+
export {};
|
|
94
|
+
//# sourceMappingURL=integrity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integrity.d.ts","sourceRoot":"","sources":["../../src/stack/integrity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AASnD,OAAO,KAAK,EACV,WAAW,EACX,iBAAiB,EACjB,eAAe,EACf,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,MAAM,EACP,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAMxC,6EAA6E;AAC7E,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,OAAO,CAAC,IAAI,CAAC,CAKf;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC,EACrC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC,IAAI,CAAC,CAUf;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,KAAK,EACZ,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,QAAQ,EACZ,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,OAAO,CAAC,IAAI,CAAC,CAgBf;AAkCD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,KAAK,GAAG,OAAO,EACtB,QAAQ,EAAE,OAAO,EACjB,IAAI,EAAE,OAAO,GACZ,IAAI,CASN;AAcD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,cAAc,EACpB,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,cAAc,EACpB,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAgBf;AAMD;;;;GAIG;AACH,QAAA,MAAM,2BAA2B,iMAIvB,CAAC;AAEX,KAAK,wBAAwB,GAAG,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AAEhF;;;;;GAKG;AACH,wBAAsB,+BAA+B,CACnD,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,IAAI,CAAC,CAmBf;AAED;;;;;GAKG;AACH,wBAAsB,kCAAkC,CACtD,IAAI,EAAE,cAAc,EACpB,YAAY,EAAE,WAAW,EAAE,GAAG,SAAS,EACvC,MAAM,GAAE,WAAW,EAAO,GACzB,OAAO,CAAC,IAAI,CAAC,CAqCf;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,CAAC,KAAK,EAAE,wBAAwB,KAAK,OAAO,GACrD,IAAI,CAON;AAMD;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAC1C,gBAAgB,EAAE,QAAQ,EAC1B,WAAW,EAAE,QAAQ,GACpB,IAAI,CAON;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,eAAe,EAAE,GAAG,IAAI,CAO1F"}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write invariants
|
|
3
|
+
* -------------------------------------------------------
|
|
4
|
+
* The integrity rules a Stack write owes whoever makes it — DID and appId
|
|
5
|
+
* bindings, containment, attachment metadata, `_config` ownership and a
|
|
6
|
+
* group's admin roster. They bind every requester, the stack owner
|
|
7
|
+
* included, which is why they live beneath `ScopedStack`'s permission
|
|
8
|
+
* gates rather than among them. Each reads unscoped: through the Stack it
|
|
9
|
+
* is handed, or a by-id `read` straight from its adapter.
|
|
10
|
+
*/
|
|
11
|
+
import { hasGroupAdmin, isGroupRecord } from '../access.js';
|
|
12
|
+
import { ARGUMENTS_INVALID, StackConflictError, StackValidationError } from '../errors.js';
|
|
13
|
+
import { bindingFieldsOf, uniqueBindingFieldsOf } from './identity-bindings.js';
|
|
14
|
+
import { filtersContent } from '../query-validation.js';
|
|
15
|
+
import { associationIdentical } from '../associations/identity.js';
|
|
16
|
+
import { validateRecordId } from './record-id.js';
|
|
17
|
+
import { baseIdOf } from '../schema.js';
|
|
18
|
+
import { findFirstMatch } from './reads.js';
|
|
19
|
+
import { SYSTEM_TYPES } from '../types/index.js';
|
|
20
|
+
// -------------------------------------------------------
|
|
21
|
+
// DID and appId bindings
|
|
22
|
+
// -------------------------------------------------------
|
|
23
|
+
/** Uniqueness for every unique binding field a newly created card claims. */
|
|
24
|
+
export async function checkBindingsOnCreate(stack, typeId, content) {
|
|
25
|
+
const family = baseIdOf(typeId);
|
|
26
|
+
for (const field of uniqueBindingFieldsOf(family)) {
|
|
27
|
+
await checkBindingUnique(stack, family, field, content[field]);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Immutability for every binding field a patch touches, then uniqueness
|
|
32
|
+
* for the subset that carries it. Fields absent from the patch carry no
|
|
33
|
+
* new claim — a content patch is a merge, so an untouched binding is the one the
|
|
34
|
+
* card already holds.
|
|
35
|
+
*/
|
|
36
|
+
export async function checkBindingsOnUpdate(stack, typeId, id, patch, existing, merged) {
|
|
37
|
+
const family = baseIdOf(typeId);
|
|
38
|
+
const unique = uniqueBindingFieldsOf(family);
|
|
39
|
+
for (const field of bindingFieldsOf(family)) {
|
|
40
|
+
if (!(field in patch))
|
|
41
|
+
continue;
|
|
42
|
+
checkBindingImmutable(family, field, existing[field], merged[field]);
|
|
43
|
+
if (unique.includes(field)) {
|
|
44
|
+
await checkBindingUnique(stack, family, field, merged[field], id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Bindings across a migration, whose content replaces the old wholesale.
|
|
50
|
+
* Immutability is asked over both families' binding fields, so a card can
|
|
51
|
+
* neither shed a DID by migrating out nor pick one up on the way in;
|
|
52
|
+
* uniqueness, of the destination only. See docs/spec/identity.md § DID bindings.
|
|
53
|
+
*/
|
|
54
|
+
export async function checkBindingsOnMigrate(stack, fromTypeId, toTypeId, id, existing, content) {
|
|
55
|
+
const fromFamily = baseIdOf(fromTypeId);
|
|
56
|
+
const toFamily = baseIdOf(toTypeId);
|
|
57
|
+
const checked = new Set();
|
|
58
|
+
for (const family of fromFamily === toFamily ? [fromFamily] : [fromFamily, toFamily]) {
|
|
59
|
+
for (const field of bindingFieldsOf(family)) {
|
|
60
|
+
if (checked.has(field))
|
|
61
|
+
continue;
|
|
62
|
+
checked.add(field);
|
|
63
|
+
checkBindingImmutable(family, field, existing[field], content[field]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const field of uniqueBindingFieldsOf(toFamily)) {
|
|
67
|
+
await checkBindingUnique(stack, toFamily, field, content[field], id);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Refuse a second card claiming a value an Actor is resolved by. A
|
|
72
|
+
* property of the set rather than the value, so no schema can say it.
|
|
73
|
+
* Read-then-write: two racing creates can both pass.
|
|
74
|
+
* See docs/spec/identity.md § DID bindings.
|
|
75
|
+
*/
|
|
76
|
+
async function checkBindingUnique(stack, family, field, value, excludeId) {
|
|
77
|
+
if (typeof value !== 'string' || value === '')
|
|
78
|
+
return;
|
|
79
|
+
const clash = await findFirstMatch((q) => stack.query(q), {
|
|
80
|
+
filter: {
|
|
81
|
+
baseId: family,
|
|
82
|
+
includeDeleted: true,
|
|
83
|
+
includeUnlisted: true,
|
|
84
|
+
...(filtersContent(stack.capabilities) && { content: { [field]: value } }),
|
|
85
|
+
},
|
|
86
|
+
}, (r) => r.id !== excludeId && r.content[field] === value);
|
|
87
|
+
if (clash) {
|
|
88
|
+
throw new StackConflictError(`Another ${family} record already claims the ${field} "${value}"`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A binding is permanent once made: uniqueness stops a second card claiming
|
|
93
|
+
* a value, but only immutability stops an existing card being moved onto
|
|
94
|
+
* one. A subject whose key changes gets a new card.
|
|
95
|
+
* See docs/spec/identity.md § DID bindings.
|
|
96
|
+
*/
|
|
97
|
+
export function checkBindingImmutable(family, field, existing, next) {
|
|
98
|
+
if (typeof existing !== 'string' || existing === '')
|
|
99
|
+
return;
|
|
100
|
+
if (next === existing)
|
|
101
|
+
return;
|
|
102
|
+
throw new StackValidationError([
|
|
103
|
+
{
|
|
104
|
+
path: field,
|
|
105
|
+
message: `${field} is immutable once set; register a new ${family} record instead`,
|
|
106
|
+
},
|
|
107
|
+
]);
|
|
108
|
+
}
|
|
109
|
+
// -------------------------------------------------------
|
|
110
|
+
// Containment
|
|
111
|
+
// -------------------------------------------------------
|
|
112
|
+
/**
|
|
113
|
+
* How far a change set's `parentId` will walk a proposed ancestor chain before refusing
|
|
114
|
+
* the move. Bounds the reads one call can cost; a hierarchy deeper than
|
|
115
|
+
* this is beyond what `parentId` is for. See docs/spec/data-model.md
|
|
116
|
+
* § Reparenting.
|
|
117
|
+
*/
|
|
118
|
+
const MAX_PARENT_DEPTH = 64;
|
|
119
|
+
/**
|
|
120
|
+
* A caller-named `parentId` is a well-formed id of a record that exists —
|
|
121
|
+
* format first, so a malformed one is a 400 naming the problem. A restore
|
|
122
|
+
* names no destination and does not ask this.
|
|
123
|
+
* See docs/spec/data-model.md § Reparenting.
|
|
124
|
+
*/
|
|
125
|
+
export async function assertParentExists(read, id, parentId) {
|
|
126
|
+
validateRecordId(parentId, 'parentId');
|
|
127
|
+
if (!(await read(parentId))) {
|
|
128
|
+
throw new StackConflictError(`Cannot parent record "${id}" to "${parentId}": no such record. A container has to ` +
|
|
129
|
+
'exist when it is named.');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Refuse an edge that would make a record its own ancestor. Walks with an
|
|
134
|
+
* unscoped `read`, since links a requester cannot read could otherwise
|
|
135
|
+
* close a cycle. Read-then-write, so racing moves can both pass.
|
|
136
|
+
* See docs/spec/data-model.md § Reparenting.
|
|
137
|
+
*/
|
|
138
|
+
export async function assertNoParentCycle(read, id, parentId) {
|
|
139
|
+
let cursor = parentId;
|
|
140
|
+
for (let depth = 0; cursor !== undefined; depth++) {
|
|
141
|
+
if (cursor === id) {
|
|
142
|
+
throw new StackConflictError(`Cannot parent record "${id}" to "${parentId}": it is a descendant of "${id}", ` +
|
|
143
|
+
'and the move would make the record its own ancestor.');
|
|
144
|
+
}
|
|
145
|
+
// The cap bounds work, and guarantees this terminates on a chain that
|
|
146
|
+
// is already cyclic. It does not refuse the move: a chain this long is
|
|
147
|
+
// past what the check can speak to, not evidence of a loop, and
|
|
148
|
+
// refusing it would claim an invariant core does not maintain.
|
|
149
|
+
if (depth >= MAX_PARENT_DEPTH)
|
|
150
|
+
return;
|
|
151
|
+
cursor = (await read(cursor))?.parentId;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// -------------------------------------------------------
|
|
155
|
+
// Attachments
|
|
156
|
+
// -------------------------------------------------------
|
|
157
|
+
/**
|
|
158
|
+
* The `_attachment@1` fields a write may not move, each with the message it
|
|
159
|
+
* is refused by. Ordered as reported. See docs/spec/attachments.md § The
|
|
160
|
+
* `_attachment` record type.
|
|
161
|
+
*/
|
|
162
|
+
const ATTACHMENT_IMMUTABLE_FIELDS = [
|
|
163
|
+
['mimeType', 'mimeType is immutable after creation; delete and re-upload to change it'],
|
|
164
|
+
['fileId', 'fileId is immutable'],
|
|
165
|
+
['size', 'size is immutable'],
|
|
166
|
+
];
|
|
167
|
+
/**
|
|
168
|
+
* mimeType belongs to the fileId: its first metadata record fixes it, and
|
|
169
|
+
* a conflicting later one is refused. Best-effort against a racing first
|
|
170
|
+
* upload; what survives the race is a deterministic winner. See
|
|
171
|
+
* docs/spec/attachments.md § The `_attachment` record type.
|
|
172
|
+
*/
|
|
173
|
+
export async function checkAttachmentMimeTypeOnCreate(stack, content) {
|
|
174
|
+
const { fileId, mimeType } = content;
|
|
175
|
+
if (typeof fileId !== 'string')
|
|
176
|
+
return; // schema validation already rejected this
|
|
177
|
+
const existing = await stack.getAttachmentRecords(fileId);
|
|
178
|
+
if (existing.length === 0)
|
|
179
|
+
return;
|
|
180
|
+
const establishedMimeType = existing[0].content.mimeType;
|
|
181
|
+
if (mimeType !== establishedMimeType) {
|
|
182
|
+
// Deliberately does not name the established mimeType — that would
|
|
183
|
+
// confirm a guessed fileId's content type. See the anti-oracle rule
|
|
184
|
+
// in docs/spec/attachments.md.
|
|
185
|
+
throw new StackValidationError([
|
|
186
|
+
{
|
|
187
|
+
path: 'mimeType',
|
|
188
|
+
message: 'mimeType conflicts with the mimeType already established for this fileId',
|
|
189
|
+
},
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* An `attachmentRecordId` must name an `_attachment` record for the same
|
|
195
|
+
* `fileId`. Pointers already in `stored` are not re-asked, so restating an
|
|
196
|
+
* association never fails for a record deleted since.
|
|
197
|
+
* See docs/spec/attachments.md § Naming the upload a reference came from.
|
|
198
|
+
*/
|
|
199
|
+
export async function checkAttachmentAssociationPointers(read, associations, stored = []) {
|
|
200
|
+
const pointed = (associations ?? []).flatMap((association) => association.kind === 'attachment' &&
|
|
201
|
+
association.attachmentRecordId !== undefined &&
|
|
202
|
+
!stored.some((a) => associationIdentical(a, association))
|
|
203
|
+
? [{ fileId: association.fileId, attachmentRecordId: association.attachmentRecordId }]
|
|
204
|
+
: []);
|
|
205
|
+
// One round trip per pointer, taken together: a change set carries a
|
|
206
|
+
// whole association list, and each pointer is an independent read.
|
|
207
|
+
const named = await Promise.all(pointed.map(({ attachmentRecordId }) => read(attachmentRecordId)));
|
|
208
|
+
pointed.forEach(({ fileId }, i) => {
|
|
209
|
+
const record = named[i];
|
|
210
|
+
const content = record?.content;
|
|
211
|
+
if (record &&
|
|
212
|
+
baseIdOf(record.typeId) === SYSTEM_TYPES.ATTACHMENT &&
|
|
213
|
+
content?.fileId === fileId) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// One message for every way of failing, so this is no existence oracle
|
|
217
|
+
// for records the caller cannot read. See docs/spec/attachments.md
|
|
218
|
+
// § Creating `_attachment@1` records directly.
|
|
219
|
+
throw new StackValidationError([
|
|
220
|
+
{
|
|
221
|
+
path: 'attachmentRecordId',
|
|
222
|
+
message: 'attachmentRecordId must name an `_attachment` record for this fileId',
|
|
223
|
+
},
|
|
224
|
+
], ARGUMENTS_INVALID);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Refuse a write that moves an `_attachment@1` record's fileId, size or
|
|
229
|
+
* mimeType. `violates` decides what counts as a move, since a patch names
|
|
230
|
+
* only what it changes while a migration re-sends all three.
|
|
231
|
+
* See docs/spec/attachments.md § The `_attachment` record type.
|
|
232
|
+
*/
|
|
233
|
+
export function assertAttachmentImmutable(violates) {
|
|
234
|
+
const errors = ATTACHMENT_IMMUTABLE_FIELDS.filter(([field]) => violates(field)).map(([path, message]) => ({ path, message }));
|
|
235
|
+
if (errors.length > 0) {
|
|
236
|
+
throw new StackValidationError(errors);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// -------------------------------------------------------
|
|
240
|
+
// Stack identity and groups
|
|
241
|
+
// -------------------------------------------------------
|
|
242
|
+
/**
|
|
243
|
+
* `_config.entityId` defines stack ownership; neither patchContent() nor
|
|
244
|
+
* restoreVersion() may change it. A conflict with stack integrity, not a
|
|
245
|
+
* schema violation — hence StackConflictError. See docs/spec.md § The
|
|
246
|
+
* `_config` record.
|
|
247
|
+
*/
|
|
248
|
+
export function checkConfigEntityIdUnchanged(existingEntityId, newEntityId) {
|
|
249
|
+
if (newEntityId !== existingEntityId) {
|
|
250
|
+
throw new StackConflictError('Cannot change _config.entityId: it defines stack ownership. ' +
|
|
251
|
+
'Ownership transfer is not a supported operation.');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Refuse a write that would leave a `_group` Record with no `admin`, asked
|
|
256
|
+
* of the roster the write would produce. An integrity rule, not a
|
|
257
|
+
* permission, so it binds the stack owner too.
|
|
258
|
+
* See docs/spec/identity.md § Group.
|
|
259
|
+
*/
|
|
260
|
+
export function assertGroupAdminRemains(record, next) {
|
|
261
|
+
if (!isGroupRecord(record))
|
|
262
|
+
return;
|
|
263
|
+
if (hasGroupAdmin(next))
|
|
264
|
+
return;
|
|
265
|
+
throw new StackConflictError(`Cannot leave group "${record.id}" without an admin: a _group record's roster keeps at ` +
|
|
266
|
+
'least one `admin` relationship association. Name the incoming admin in the same write.');
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=integrity.js.map
|