@haverstack/core 0.40.0 → 0.41.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/dist/grants.d.ts +2 -1
- package/dist/grants.d.ts.map +1 -1
- package/dist/grants.js +3 -1
- package/dist/grants.js.map +1 -1
- package/dist/identity-bindings.d.ts.map +1 -1
- package/dist/identity-bindings.js +3 -0
- package/dist/identity-bindings.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +144 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +223 -0
- package/dist/install.js.map +1 -0
- package/dist/scoped-stack.d.ts +23 -6
- package/dist/scoped-stack.d.ts.map +1 -1
- package/dist/scoped-stack.js +92 -10
- package/dist/scoped-stack.js.map +1 -1
- package/dist/stack.d.ts +63 -2
- package/dist/stack.d.ts.map +1 -1
- package/dist/stack.js +328 -3
- package/dist/stack.js.map +1 -1
- package/dist/types.d.ts +28 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +15 -6
- package/dist/validate.js.map +1 -1
- package/dist/wire-body.d.ts +10 -2
- package/dist/wire-body.d.ts.map +1 -1
- package/dist/wire-body.js +60 -2
- package/dist/wire-body.js.map +1 -1
- package/dist/wire-entry.d.ts +1 -1
- package/dist/wire-entry.d.ts.map +1 -1
- package/dist/wire-entry.js +1 -1
- package/dist/wire-entry.js.map +1 -1
- package/package.json +1 -1
package/dist/stack.js
CHANGED
|
@@ -25,6 +25,7 @@ import { UseAfterCloseError, InvalidAdapterError, StackConflictError, StackMigra
|
|
|
25
25
|
import { assertQueryCapabilities, assertSortCapability, assertValidAssociationFilters, assertValidBaseIdFilter, assertValidJournalQuery, assertValidVersionsQuery, assertValidSort, normalizeSort, assertAssociationEdits, assertAuthorityAssociations, assertDataAssociations, filtersContent, validateAssociations, } from './query-validation.js';
|
|
26
26
|
import { GRANT_ACTION_SET, READ_COMPANIONS, grantConveys, grantReach, matchesGrantTarget, validateGrantTarget, validateGrantee, validateGrantBaseId, grantCoversGrantee, loadGrantRecords, } from './grants.js';
|
|
27
27
|
import { bindingFieldsOf, uniqueBindingFieldsOf } from './identity-bindings.js';
|
|
28
|
+
import { claimedFamilies, familyStanding, grantIsRequest, installAppLink, installGrantLink, installReader, linkedIds, namespaceOf, ownTypeIds, planFingerprint, sameRequest, snapshotManifest, validateInstall, INSTALL_APP_LABEL, INSTALL_GRANT_LABEL, } from './install.js';
|
|
28
29
|
import { assertAttachmentSize, assertContentSize } from './limits.js';
|
|
29
30
|
import { validateParentId, validateRecordId, validateClockField, validateIdTimestampSkew, DEFAULT_ID_TIMESTAMP_SKEW_MS, } from './record-id.js';
|
|
30
31
|
import { queryAllPages, findFirstMatch, lookupEntityByDid, MAX_QUERY_LIMIT, } from './stack-reads.js';
|
|
@@ -376,8 +377,13 @@ export class Stack {
|
|
|
376
377
|
* the only way disk state changes version. Sweeps soft-deleted records
|
|
377
378
|
* too, validates each result before writing, and aborts on the first
|
|
378
379
|
* validation failure. See docs/spec/data-model.md § Type migrations.
|
|
380
|
+
*
|
|
381
|
+
* `sweep: 'listed'` is for a contained app over `APIAdapter`, which can
|
|
382
|
+
* neither enumerate unlisted records nor read a deleted one's content:
|
|
383
|
+
* it migrates live, listed records and counts the deleted ones it passed
|
|
384
|
+
* over in `skipped`. See docs/spec/apps.md § Migrating an installed app's types.
|
|
379
385
|
*/
|
|
380
|
-
async migrateAll(baseId) {
|
|
386
|
+
async migrateAll(baseId, opts = {}) {
|
|
381
387
|
this.assertOpen();
|
|
382
388
|
const problem = familyIdProblem(baseId, 'migrateAll');
|
|
383
389
|
if (problem)
|
|
@@ -387,7 +393,9 @@ export class Stack {
|
|
|
387
393
|
if (familyTypeIds.length === 0) {
|
|
388
394
|
throw new StackMigrationError(`migrateAll: no registered types found for baseId "${baseId}"`);
|
|
389
395
|
}
|
|
396
|
+
const listedOnly = opts.sweep === 'listed';
|
|
390
397
|
let migrated = 0;
|
|
398
|
+
let skipped = 0;
|
|
391
399
|
for (const typeId of familyTypeIds) {
|
|
392
400
|
const latestId = this.latestTypeId(typeId);
|
|
393
401
|
if (typeId === latestId)
|
|
@@ -402,11 +410,15 @@ export class Stack {
|
|
|
402
410
|
let cursor;
|
|
403
411
|
do {
|
|
404
412
|
const result = await this.adapter.queryRecords({
|
|
405
|
-
filter: { typeId, includeDeleted: true, includeUnlisted:
|
|
413
|
+
filter: { typeId, includeDeleted: true, includeUnlisted: !listedOnly },
|
|
406
414
|
limit: 100,
|
|
407
415
|
cursor,
|
|
408
416
|
});
|
|
409
417
|
for (const record of result.records) {
|
|
418
|
+
if (listedOnly && record.deletedAt) {
|
|
419
|
+
skipped++;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
410
422
|
// Same checked path commitMigration() takes — a migration
|
|
411
423
|
// function is no more entitled to move a DID binding or repoint
|
|
412
424
|
// an attachment than a request body is. No ifVersion: a batch
|
|
@@ -417,7 +429,7 @@ export class Stack {
|
|
|
417
429
|
cursor = result.cursor ?? undefined;
|
|
418
430
|
} while (cursor);
|
|
419
431
|
}
|
|
420
|
-
return { migrated };
|
|
432
|
+
return { migrated, skipped };
|
|
421
433
|
}
|
|
422
434
|
async create(typeIdOrHandle, input, opts = {}) {
|
|
423
435
|
if (isTypeHandle(typeIdOrHandle))
|
|
@@ -450,6 +462,7 @@ export class Stack {
|
|
|
450
462
|
...validateContent(content, type.schema),
|
|
451
463
|
...validateGrantee(typeId, content),
|
|
452
464
|
...validateGrantBaseId(typeId, content),
|
|
465
|
+
...validateInstall(typeId, content),
|
|
453
466
|
...validateAssociations(opts.permissions, 'permissions'),
|
|
454
467
|
...validatePermissions(opts.permissions),
|
|
455
468
|
...validateAssociations(opts.associations),
|
|
@@ -694,6 +707,7 @@ export class Stack {
|
|
|
694
707
|
...validateContent(merged, type.schema),
|
|
695
708
|
...validateGrantee(existing.typeId, merged),
|
|
696
709
|
...validateGrantBaseId(existing.typeId, merged),
|
|
710
|
+
...validateInstall(existing.typeId, merged),
|
|
697
711
|
];
|
|
698
712
|
if (contentErrors.length > 0)
|
|
699
713
|
throw new StackValidationError(contentErrors);
|
|
@@ -1115,6 +1129,7 @@ export class Stack {
|
|
|
1115
1129
|
...validateContent(target.content, type.schema),
|
|
1116
1130
|
...validateGrantee(target.typeId, target.content),
|
|
1117
1131
|
...validateGrantBaseId(target.typeId, target.content),
|
|
1132
|
+
...validateInstall(target.typeId, target.content),
|
|
1118
1133
|
];
|
|
1119
1134
|
if (errors.length > 0) {
|
|
1120
1135
|
throw new StackValidationError(errors);
|
|
@@ -1193,6 +1208,7 @@ export class Stack {
|
|
|
1193
1208
|
...validateContent(content, type.schema),
|
|
1194
1209
|
...validateGrantee(toTypeId, content),
|
|
1195
1210
|
...validateGrantBaseId(toTypeId, content),
|
|
1211
|
+
...validateInstall(toTypeId, content),
|
|
1196
1212
|
];
|
|
1197
1213
|
if (errors.length > 0) {
|
|
1198
1214
|
throw new StackValidationError(errors);
|
|
@@ -1849,6 +1865,294 @@ export class Stack {
|
|
|
1849
1865
|
return matches;
|
|
1850
1866
|
}
|
|
1851
1867
|
// -------------------------------------------------------
|
|
1868
|
+
// App installs
|
|
1869
|
+
// -------------------------------------------------------
|
|
1870
|
+
/**
|
|
1871
|
+
* What applying `manifest` for the key `did` would change, for the owner
|
|
1872
|
+
* to review before installApp(). Writes nothing. Refuses outright what no
|
|
1873
|
+
* approval could make valid: a type outside the app's own namespace and
|
|
1874
|
+
* the commons, a request the grant rules refuse, or a `did` already
|
|
1875
|
+
* registered to a different app. See docs/spec/apps.md § Plan, then apply.
|
|
1876
|
+
*/
|
|
1877
|
+
async planInstall(submitted, opts) {
|
|
1878
|
+
this.assertOpen();
|
|
1879
|
+
const { did } = opts;
|
|
1880
|
+
const manifest = snapshotManifest(submitted);
|
|
1881
|
+
this.checkManifest(manifest, did);
|
|
1882
|
+
const installs = await this.loadInstalls();
|
|
1883
|
+
const existing = installs.find((r) => r.content.appId === manifest.appId) ?? null;
|
|
1884
|
+
const ownVersions = ownTypeIds(manifest);
|
|
1885
|
+
const ownFamilies = new Set(ownVersions.map(baseIdOf));
|
|
1886
|
+
const card = await this.findAppCard(did);
|
|
1887
|
+
if (card && card.content.appId !== manifest.appId) {
|
|
1888
|
+
throw new StackConflictError(`${did} is registered to "${card.content.appId}", not "${manifest.appId}"`);
|
|
1889
|
+
}
|
|
1890
|
+
const claimed = existing ? claimedFamilies(existing.content) : new Set();
|
|
1891
|
+
const defined = new Set(existing?.content.defines ?? []);
|
|
1892
|
+
const prior = existing?.content.requests ?? [];
|
|
1893
|
+
const foreignRequests = [];
|
|
1894
|
+
for (const r of manifest.requests) {
|
|
1895
|
+
const standing = familyStanding(r.baseId, manifest.appId);
|
|
1896
|
+
if (standing === 'own')
|
|
1897
|
+
continue;
|
|
1898
|
+
const owner = standing === 'foreign'
|
|
1899
|
+
? namespaceOf(r.baseId)
|
|
1900
|
+
: standing === 'commons'
|
|
1901
|
+
? 'commons'
|
|
1902
|
+
: 'system';
|
|
1903
|
+
foreignRequests.push({ ...r, owner });
|
|
1904
|
+
}
|
|
1905
|
+
const typeChanges = [];
|
|
1906
|
+
for (const t of manifest.types) {
|
|
1907
|
+
const current = await this.getTypeCached(t.id);
|
|
1908
|
+
if (!current)
|
|
1909
|
+
typeChanges.push({ id: t.id, change: 'new' });
|
|
1910
|
+
else if (current.schemaHash !== (await hashSchema(t.schema))) {
|
|
1911
|
+
// Refused here, not left to defineType(): installApp() defines types one
|
|
1912
|
+
// by one, so a drift found there leaves the earlier ones written.
|
|
1913
|
+
const violations = diffSchemas(current.schema, t.schema);
|
|
1914
|
+
if (violations.length > 0)
|
|
1915
|
+
throw new StackSchemaDriftError(t.id, violations);
|
|
1916
|
+
typeChanges.push({ id: t.id, change: 'schema' });
|
|
1917
|
+
}
|
|
1918
|
+
else if (current.name !== t.name)
|
|
1919
|
+
typeChanges.push({ id: t.id, change: 'name' });
|
|
1920
|
+
}
|
|
1921
|
+
const linkedKeys = [];
|
|
1922
|
+
for (const id of existing ? linkedIds(existing, INSTALL_APP_LABEL) : []) {
|
|
1923
|
+
const key = (await this.get(id))?.content?.did;
|
|
1924
|
+
if (typeof key === 'string')
|
|
1925
|
+
linkedKeys.push(key);
|
|
1926
|
+
}
|
|
1927
|
+
return {
|
|
1928
|
+
manifest,
|
|
1929
|
+
did,
|
|
1930
|
+
existing,
|
|
1931
|
+
newFamilies: [...ownFamilies].filter((f) => !claimed.has(f)),
|
|
1932
|
+
newVersions: ownVersions.filter((id) => !defined.has(id)),
|
|
1933
|
+
requestsAdded: manifest.requests.filter((r) => !prior.some((p) => sameRequest(p, r))),
|
|
1934
|
+
requestsRemoved: prior.filter((p) => !manifest.requests.some((r) => sameRequest(p, r))),
|
|
1935
|
+
foreignRequests,
|
|
1936
|
+
typeChanges,
|
|
1937
|
+
newKey: !existing ||
|
|
1938
|
+
!card ||
|
|
1939
|
+
card.deletedAt !== undefined ||
|
|
1940
|
+
!linkedIds(existing, INSTALL_APP_LABEL).includes(card.id),
|
|
1941
|
+
linkedKeys,
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Apply an approved plan: define the manifest's types, register the
|
|
1946
|
+
* key's `_app` card if it has none, write the `_install` Record, and
|
|
1947
|
+
* bring every linked key's grants to exactly what the manifest requests.
|
|
1948
|
+
* Refuses a plan the stack has moved on from with StackConflictError, so
|
|
1949
|
+
* what is applied is what was approved. Reinstalls a soft-deleted
|
|
1950
|
+
* install. See docs/spec/apps.md § Plan, then apply.
|
|
1951
|
+
*/
|
|
1952
|
+
async installApp(plan) {
|
|
1953
|
+
this.assertOpen();
|
|
1954
|
+
const fresh = await this.planInstall(plan.manifest, { did: plan.did });
|
|
1955
|
+
if (planFingerprint(fresh) !== planFingerprint(plan)) {
|
|
1956
|
+
throw new StackConflictError(`The stack changed since the install of "${plan.manifest.appId}" was planned; plan it again`);
|
|
1957
|
+
}
|
|
1958
|
+
const { manifest, did, existing } = fresh;
|
|
1959
|
+
for (const type of manifest.types)
|
|
1960
|
+
await this.defineType(type);
|
|
1961
|
+
const card = await this.ensureAppCard(manifest, did);
|
|
1962
|
+
const defines = [...new Set([...(existing?.content.defines ?? []), ...ownTypeIds(manifest)])];
|
|
1963
|
+
const requests = manifest.requests.map((r) => ({
|
|
1964
|
+
baseId: r.baseId,
|
|
1965
|
+
actions: [...r.actions],
|
|
1966
|
+
}));
|
|
1967
|
+
let install;
|
|
1968
|
+
if (!existing) {
|
|
1969
|
+
install = await this.create(`${SYSTEM_TYPES.INSTALL}@1`, {
|
|
1970
|
+
appId: manifest.appId,
|
|
1971
|
+
name: manifest.name,
|
|
1972
|
+
...(manifest.version !== undefined && { version: manifest.version }),
|
|
1973
|
+
defines,
|
|
1974
|
+
requests,
|
|
1975
|
+
}, { associations: [installAppLink(card.id)] });
|
|
1976
|
+
}
|
|
1977
|
+
else {
|
|
1978
|
+
const current = existing.deletedAt ? await this.undelete(existing.id) : existing;
|
|
1979
|
+
install = await this.patchContent(existing.id, { name: manifest.name, version: manifest.version ?? null, defines, requests }, { ifVersion: current.version });
|
|
1980
|
+
if (!linkedIds(install, INSTALL_APP_LABEL).includes(card.id)) {
|
|
1981
|
+
install = await this.associate(install.id, [installAppLink(card.id)]);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
return (await this.reconcileInstallGrants(install));
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* Withdraw every grant an install produced and soft-delete it. The app's
|
|
1988
|
+
* records and `_app` cards stay: the data is the owner's, and the cards
|
|
1989
|
+
* are what its records' attribution resolves through. Returns the
|
|
1990
|
+
* install's tombstone. See docs/spec/apps.md § Uninstalling.
|
|
1991
|
+
*/
|
|
1992
|
+
async uninstallApp(appId) {
|
|
1993
|
+
this.assertOpen();
|
|
1994
|
+
const install = (await this.loadInstalls()).find((r) => r.content.appId === appId && !r.deletedAt);
|
|
1995
|
+
if (!install)
|
|
1996
|
+
throw new StackNotFoundError(`No install for "${appId}"`);
|
|
1997
|
+
const edits = [];
|
|
1998
|
+
for (const id of linkedIds(install, INSTALL_GRANT_LABEL)) {
|
|
1999
|
+
if (await this.get(id))
|
|
2000
|
+
await this.delete(id);
|
|
2001
|
+
edits.push({ op: 'remove', association: installGrantLink(id) });
|
|
2002
|
+
}
|
|
2003
|
+
if (edits.length > 0)
|
|
2004
|
+
await this.amendAssociations(install.id, edits);
|
|
2005
|
+
const { record } = await this.deleteAndReturn(install.id);
|
|
2006
|
+
return record;
|
|
2007
|
+
}
|
|
2008
|
+
/** A manifest's own shape, and every request held to grantType()'s rules. */
|
|
2009
|
+
checkManifest(manifest, did) {
|
|
2010
|
+
const errors = [];
|
|
2011
|
+
if (typeof did !== 'string' || !did.startsWith('did:')) {
|
|
2012
|
+
errors.push({ path: 'did', message: 'Expected a DID' });
|
|
2013
|
+
}
|
|
2014
|
+
if (typeof manifest.appId !== 'string' || manifest.appId === '') {
|
|
2015
|
+
errors.push({ path: 'appId', message: 'Expected a non-empty appId' });
|
|
2016
|
+
}
|
|
2017
|
+
if (typeof manifest.name !== 'string' || manifest.name === '') {
|
|
2018
|
+
errors.push({ path: 'name', message: 'Expected a non-empty name' });
|
|
2019
|
+
}
|
|
2020
|
+
const seen = new Set();
|
|
2021
|
+
manifest.types.forEach((t, i) => {
|
|
2022
|
+
if (seen.has(t.id)) {
|
|
2023
|
+
errors.push({ path: `types[${i}].id`, message: `"${t.id}" is listed more than once` });
|
|
2024
|
+
}
|
|
2025
|
+
seen.add(t.id);
|
|
2026
|
+
const schemaPath = `types[${i}].schema`;
|
|
2027
|
+
const shapeErrors = validateSchemaShape(t.schema, schemaPath);
|
|
2028
|
+
errors.push(...shapeErrors);
|
|
2029
|
+
if (shapeErrors.length === 0) {
|
|
2030
|
+
const schema = t.schema;
|
|
2031
|
+
for (const e of validateSchemaReservedNames(schema)) {
|
|
2032
|
+
errors.push({ ...e, path: `${schemaPath}.${e.path}` });
|
|
2033
|
+
}
|
|
2034
|
+
validateSchemaFieldNames(schema, schemaPath, errors);
|
|
2035
|
+
}
|
|
2036
|
+
const parsed = parseTypeId(t.id);
|
|
2037
|
+
if (!parsed) {
|
|
2038
|
+
errors.push({ path: `types[${i}].id`, message: 'Expected a versioned TypeId' });
|
|
2039
|
+
}
|
|
2040
|
+
else {
|
|
2041
|
+
const standing = familyStanding(parsed.baseId, manifest.appId);
|
|
2042
|
+
if (standing === 'system' || standing === 'foreign') {
|
|
2043
|
+
errors.push({
|
|
2044
|
+
path: `types[${i}].id`,
|
|
2045
|
+
message: standing === 'system'
|
|
2046
|
+
? `"${parsed.baseId}" is a system type; no app can define it`
|
|
2047
|
+
: `"${parsed.baseId}" is outside the namespace "${manifest.appId}"; request access to it instead of defining it`,
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
});
|
|
2052
|
+
if (errors.length > 0)
|
|
2053
|
+
throw new StackValidationError(errors);
|
|
2054
|
+
for (const r of manifest.requests)
|
|
2055
|
+
this.checkGrantValid(r.baseId, r.actions);
|
|
2056
|
+
}
|
|
2057
|
+
/** Every `_install` Record, deleted and unlisted included — a deleted install keeps its claims. */
|
|
2058
|
+
async loadInstalls() {
|
|
2059
|
+
const records = await queryAllPages((q) => this.query(q), {
|
|
2060
|
+
filter: { baseId: SYSTEM_TYPES.INSTALL, includeDeleted: true, includeUnlisted: true },
|
|
2061
|
+
});
|
|
2062
|
+
return records;
|
|
2063
|
+
}
|
|
2064
|
+
/** The `_app` card claiming `did`, deleted and unlisted included. */
|
|
2065
|
+
findAppCard(did) {
|
|
2066
|
+
return findFirstMatch((q) => this.query(q), {
|
|
2067
|
+
filter: {
|
|
2068
|
+
baseId: SYSTEM_TYPES.APP,
|
|
2069
|
+
includeDeleted: true,
|
|
2070
|
+
includeUnlisted: true,
|
|
2071
|
+
...(filtersContent(this.capabilities) && { content: { did } }),
|
|
2072
|
+
},
|
|
2073
|
+
}, (r) => r.content.did === did);
|
|
2074
|
+
}
|
|
2075
|
+
/** The key's `_app` card, created or undeleted as needed. */
|
|
2076
|
+
async ensureAppCard(manifest, did) {
|
|
2077
|
+
const card = await this.findAppCard(did);
|
|
2078
|
+
if (!card) {
|
|
2079
|
+
return this.create(`${SYSTEM_TYPES.APP}@1`, {
|
|
2080
|
+
appId: manifest.appId,
|
|
2081
|
+
name: manifest.name,
|
|
2082
|
+
...(manifest.version !== undefined && { version: manifest.version }),
|
|
2083
|
+
did,
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
return card.deletedAt ? this.undelete(card.id) : card;
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Make the grants linked to `install` exactly its `requests`, once per
|
|
2090
|
+
* live linked key: withdraw any that no longer match, write any missing,
|
|
2091
|
+
* and drop links to grants that are gone.
|
|
2092
|
+
*/
|
|
2093
|
+
async reconcileInstallGrants(install) {
|
|
2094
|
+
const { requests } = install.content;
|
|
2095
|
+
const dids = [];
|
|
2096
|
+
for (const id of linkedIds(install, INSTALL_APP_LABEL)) {
|
|
2097
|
+
const did = (await this.get(id))?.content?.did;
|
|
2098
|
+
if (typeof did === 'string')
|
|
2099
|
+
dids.push(did);
|
|
2100
|
+
}
|
|
2101
|
+
const edits = [];
|
|
2102
|
+
const held = [];
|
|
2103
|
+
for (const id of linkedIds(install, INSTALL_GRANT_LABEL)) {
|
|
2104
|
+
const grant = await this.get(id);
|
|
2105
|
+
const content = grant?.content;
|
|
2106
|
+
const wanted = content !== undefined &&
|
|
2107
|
+
baseIdOf(grant.typeId) === SYSTEM_TYPES.GRANT &&
|
|
2108
|
+
dids.some((did) => requests.some((r) => grantIsRequest(content, r, did)));
|
|
2109
|
+
if (wanted) {
|
|
2110
|
+
held.push(content);
|
|
2111
|
+
continue;
|
|
2112
|
+
}
|
|
2113
|
+
if (grant)
|
|
2114
|
+
await this.delete(id);
|
|
2115
|
+
edits.push({ op: 'remove', association: installGrantLink(id) });
|
|
2116
|
+
}
|
|
2117
|
+
for (const did of dids) {
|
|
2118
|
+
for (const r of requests) {
|
|
2119
|
+
if (held.some((g) => grantIsRequest(g, r, did)))
|
|
2120
|
+
continue;
|
|
2121
|
+
const grant = await this.grantType(r.baseId, {
|
|
2122
|
+
actions: r.actions,
|
|
2123
|
+
grantee: { kind: 'entity', entityId: did },
|
|
2124
|
+
});
|
|
2125
|
+
held.push(grant.content);
|
|
2126
|
+
edits.push({ op: 'add', association: installGrantLink(grant.id) });
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
let result = edits.length > 0 ? await this.amendAssociations(install.id, edits) : install;
|
|
2130
|
+
// Each linked key may read its own install, which is how an app learns
|
|
2131
|
+
// what was approved; a key of this app no longer linked may not.
|
|
2132
|
+
// See docs/spec/apps.md § Over the wire.
|
|
2133
|
+
const appKeys = new Set((await queryAllPages((q) => this.query(q), {
|
|
2134
|
+
filter: { baseId: SYSTEM_TYPES.APP, includeDeleted: true, includeUnlisted: true },
|
|
2135
|
+
}))
|
|
2136
|
+
.map((r) => r.content)
|
|
2137
|
+
.filter((c) => c.appId === install.content.appId)
|
|
2138
|
+
.map((c) => c.did));
|
|
2139
|
+
const readerOf = (p) => p.kind === 'permission' && p.label === 'read' && p.grantee.kind === 'entity'
|
|
2140
|
+
? p.grantee.entityId
|
|
2141
|
+
: null;
|
|
2142
|
+
const current = (result.permissions ?? []).map(readerOf);
|
|
2143
|
+
const access = [
|
|
2144
|
+
...dids
|
|
2145
|
+
.filter((did) => !current.includes(did))
|
|
2146
|
+
.map((entityId) => ({ op: 'add', association: installReader(entityId) })),
|
|
2147
|
+
...current
|
|
2148
|
+
.filter((did) => did !== null && appKeys.has(did) && !dids.includes(did))
|
|
2149
|
+
.map((entityId) => ({ op: 'remove', association: installReader(entityId) })),
|
|
2150
|
+
];
|
|
2151
|
+
if (access.length > 0)
|
|
2152
|
+
result = await this.amendAccess(install.id, access);
|
|
2153
|
+
return result;
|
|
2154
|
+
}
|
|
2155
|
+
// -------------------------------------------------------
|
|
1852
2156
|
// Private helpers
|
|
1853
2157
|
// -------------------------------------------------------
|
|
1854
2158
|
/**
|
|
@@ -1962,6 +2266,27 @@ export class Stack {
|
|
|
1962
2266
|
filename: { kind: 'string' },
|
|
1963
2267
|
},
|
|
1964
2268
|
});
|
|
2269
|
+
await this.defineType({
|
|
2270
|
+
id: `${SYSTEM_TYPES.INSTALL}@1`,
|
|
2271
|
+
name: 'Install',
|
|
2272
|
+
schema: {
|
|
2273
|
+
appId: { kind: 'string', required: true },
|
|
2274
|
+
name: { kind: 'string', required: true },
|
|
2275
|
+
version: { kind: 'string' },
|
|
2276
|
+
defines: { kind: 'array', items: { kind: 'string' }, required: true },
|
|
2277
|
+
requests: {
|
|
2278
|
+
kind: 'array',
|
|
2279
|
+
required: true,
|
|
2280
|
+
items: {
|
|
2281
|
+
kind: 'object',
|
|
2282
|
+
properties: {
|
|
2283
|
+
baseId: { kind: 'string', required: true },
|
|
2284
|
+
actions: { kind: 'array', items: { kind: 'string' }, required: true },
|
|
2285
|
+
},
|
|
2286
|
+
},
|
|
2287
|
+
},
|
|
2288
|
+
},
|
|
2289
|
+
});
|
|
1965
2290
|
}
|
|
1966
2291
|
/**
|
|
1967
2292
|
* Fast-fail for the ifVersion precondition using the already-fetched
|