@powerhousedao/reactor 6.2.3-dev.25 → 6.2.3-dev.27
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/{build-worker-executor-DPGjh9Uc.js → build-worker-executor-Bvoba-AV.js} +2 -2
- package/dist/{build-worker-executor-DPGjh9Uc.js.map → build-worker-executor-Bvoba-AV.js.map} +1 -1
- package/dist/{document-indexer-zbdzqZ3K.js → document-indexer-BVTGbEB0.js} +6 -14
- package/dist/document-indexer-BVTGbEB0.js.map +1 -0
- package/dist/entry.js +2 -2
- package/dist/{forwarding-logger-CSsCJ38f.js → forwarding-logger-CnuVDVze.js} +57 -15
- package/dist/forwarding-logger-CnuVDVze.js.map +1 -0
- package/dist/index.d.ts +116 -73
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +460 -202
- package/dist/index.js.map +1 -1
- package/dist/projection-entry.js +3 -3
- package/dist/{projection-shard-manager-B-j8VYzA.js → projection-shard-manager-BpcqDtnQ.js} +2 -2
- package/dist/{projection-shard-manager-B-j8VYzA.js.map → projection-shard-manager-BpcqDtnQ.js.map} +1 -1
- package/dist/{worker-handle-CwWNTpUg.js → worker-handle-C2rLUHwv.js} +2 -2
- package/dist/{worker-handle-CwWNTpUg.js.map → worker-handle-C2rLUHwv.js.map} +1 -1
- package/package.json +13 -6
- package/dist/document-indexer-zbdzqZ3K.js.map +0 -1
- package/dist/forwarding-logger-CSsCJ38f.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { $ as DocumentNotFoundError, A as ModuleNotFoundError, B as authDecisionModel, C as DocumentMetaCache, D as DuplicateManifestError, E as JobExecutorEventTypes, F as summarizeSubmittedActions, G as DocumentExistence, H as APPEND_CONDITION_FAILED_PREFIX, I as targetDocumentId, J as RevisionMismatchError, K as DuplicateOperationError, L as decideAtHead, M as createConsistencyToken, N as createEmptyConsistencyToken, O as DuplicateModuleError, P as submittedActionIds, Q as DocumentDeletedError, R as selectDecisionModel, S as KyselyOperationIndex, T as DEFAULT_DEFERRED_JOB_TTL_MS, U as AppendConditionFailedError, V as buildDecisionModel, W as DocumentAlreadyExistsError, X as AuthTimestampNotMonotonicError, Y as AuthEnforcementDisabledError, Z as AuthorizationDeniedError, _ as DriveCollectionId, a as DEFAULT_DRIVE_CONTAINER_TYPES, at as EventBusAggregateError, b as EventBus, c as runMigrations, ct as parsePagingOptions, d as KyselyOperationStore, dt as __exportAll, et as ExcessiveReshuffleError, f as KyselyKeyframeStore, g as PassthroughSigner, h as verifyActionSignature, it as UpgradePreconditionFailedError, j as GATED_DOCUMENT_ACTIONS, k as InvalidModuleError, l as createForwardingPoolInstrumentation, lt as throwIfAborted, m as SimpleJobExecutor, n as errorToInfo, nt as InvalidSignatureError, o as REACTOR_SCHEMA, ot as ReactorEventTypes, p as DocumentModelRegistry, q as OptimisticLockError, r as sanitizeArg, rt as RelationshipNotFoundError, s as getMigrationStatus, st as matchesScope, t as createForwardingLogger, tt as InvalidOperationTimestampError, u as instrumentPgPool, v as KyselyExecutionScope, w as CollectionMembershipCache, x as KyselyWriteCache, y as resolveFeatureFlags, z as documentDecisionModel } from "./forwarding-logger-
|
|
1
|
+
import { $ as DocumentNotFoundError, A as ModuleNotFoundError, B as authDecisionModel, C as DocumentMetaCache, D as DuplicateManifestError, E as JobExecutorEventTypes, F as summarizeSubmittedActions, G as DocumentExistence, H as APPEND_CONDITION_FAILED_PREFIX, I as targetDocumentId, J as RevisionMismatchError, K as DuplicateOperationError, L as decideAtHead, M as createConsistencyToken, N as createEmptyConsistencyToken, O as DuplicateModuleError, P as submittedActionIds, Q as DocumentDeletedError, R as selectDecisionModel, S as KyselyOperationIndex, T as DEFAULT_DEFERRED_JOB_TTL_MS, U as AppendConditionFailedError, V as buildDecisionModel, W as DocumentAlreadyExistsError, X as AuthTimestampNotMonotonicError, Y as AuthEnforcementDisabledError, Z as AuthorizationDeniedError, _ as DriveCollectionId, a as DEFAULT_DRIVE_CONTAINER_TYPES, at as EventBusAggregateError, b as EventBus, c as runMigrations, ct as parsePagingOptions, d as KyselyOperationStore, dt as __exportAll, et as ExcessiveReshuffleError, f as KyselyKeyframeStore, g as PassthroughSigner, h as verifyActionSignature, it as UpgradePreconditionFailedError, j as GATED_DOCUMENT_ACTIONS, k as InvalidModuleError, l as createForwardingPoolInstrumentation, lt as throwIfAborted, m as SimpleJobExecutor, n as errorToInfo, nt as InvalidSignatureError, o as REACTOR_SCHEMA, ot as ReactorEventTypes, p as DocumentModelRegistry, q as OptimisticLockError, r as sanitizeArg, rt as RelationshipNotFoundError, s as getMigrationStatus, st as matchesScope, t as createForwardingLogger, tt as InvalidOperationTimestampError, u as instrumentPgPool, v as KyselyExecutionScope, w as CollectionMembershipCache, x as KyselyWriteCache, y as resolveFeatureFlags, z as documentDecisionModel } from "./forwarding-logger-CnuVDVze.js";
|
|
2
2
|
import { i as WorkerInitFailedError, r as WorkerExitedError, t as WorkerAbortTimeoutError } from "./errors-BM_ULVWd.js";
|
|
3
|
-
import { a as KyselyDocumentView, c as ReadModelCoordinator, d as DEFAULT_READ_MODEL_YIELD_DEADLINE_MS, f as defaultReadModelIndexingConfig, i as DeletedDocumentRead, l as BaseReadModel, n as ConsistencyTracker, o as DOCUMENT_INDEXER_READ_MODEL, p as unchunkedReadModelIndexingConfig, r as makeConsistencyKey, s as DOCUMENT_VIEW_READ_MODEL, t as KyselyDocumentIndexer, u as DEFAULT_COMMIT_CHUNK_SIZE } from "./document-indexer-
|
|
3
|
+
import { a as KyselyDocumentView, c as ReadModelCoordinator, d as DEFAULT_READ_MODEL_YIELD_DEADLINE_MS, f as defaultReadModelIndexingConfig, i as DeletedDocumentRead, l as BaseReadModel, n as ConsistencyTracker, o as DOCUMENT_INDEXER_READ_MODEL, p as unchunkedReadModelIndexingConfig, r as makeConsistencyKey, s as DOCUMENT_VIEW_READ_MODEL, t as KyselyDocumentIndexer, u as DEFAULT_COMMIT_CHUNK_SIZE } from "./document-indexer-BVTGbEB0.js";
|
|
4
4
|
import { DEFAULT_SIGNATURE_POLICY, DowngradeNotSupportedError, MAX_AUTH_GRANTS, UnsupportedDocumentModelVersionError, actionSignerIdentity, actionSigningTarget, actions, actions as documentActions, createCopyHeader, decide, deserializeSignature, garbageCollect, generateId, groupDocumentType, groupMembershipActionTypes, hashDocumentStateForScope, normalizeDocumentModelVersion, requestedSignaturePolicy, sortOperations, toTransportAction, withSignaturePolicy } from "@powerhousedao/shared/document-model";
|
|
5
5
|
import { addFile, addFolder, copyNode, deleteNode, driveCreateDocument, generateNodesCopy, getDescendants, handleTargetNameCollisions, isFileNode, isFolderNode, moveNode, updateNode } from "@powerhousedao/shared/document-drive";
|
|
6
6
|
import { v4 } from "uuid";
|
|
@@ -504,8 +504,7 @@ var DriveClient = class {
|
|
|
504
504
|
return {
|
|
505
505
|
results: filtered.slice(startIndex, endIndex),
|
|
506
506
|
options: effective,
|
|
507
|
-
...endIndex < filtered.length ? { nextCursor: String(endIndex) } : {}
|
|
508
|
-
totalCount: filtered.length
|
|
507
|
+
...endIndex < filtered.length ? { nextCursor: String(endIndex) } : {}
|
|
509
508
|
};
|
|
510
509
|
}
|
|
511
510
|
/**
|
|
@@ -722,6 +721,53 @@ let DocumentChangeType = /* @__PURE__ */ function(DocumentChangeType) {
|
|
|
722
721
|
* argument covers. Grants gate domain-scope reads only.
|
|
723
722
|
*/
|
|
724
723
|
const ALWAYS_READABLE_SCOPES = /* @__PURE__ */ new Set(["auth", "document"]);
|
|
724
|
+
const DECLARED_DOMAIN_SCOPES = ["global", "local"];
|
|
725
|
+
/**
|
|
726
|
+
* The scopes grants gate: the declared ones, and any other the document holds.
|
|
727
|
+
* A document fetched for a narrowed view holds only that view's scopes.
|
|
728
|
+
*/
|
|
729
|
+
function domainScopesOf(document) {
|
|
730
|
+
const state = document.state;
|
|
731
|
+
const held = Object.keys(state ?? {}).filter((scope) => !ALWAYS_READABLE_SCOPES.has(scope));
|
|
732
|
+
return [.../* @__PURE__ */ new Set([...DECLARED_DOMAIN_SCOPES, ...held])];
|
|
733
|
+
}
|
|
734
|
+
function allowsSomeDomainScope(document, readable) {
|
|
735
|
+
return domainScopesOf(document).some(readable);
|
|
736
|
+
}
|
|
737
|
+
/** A document with no state at all is not policied, so nothing withholds it. */
|
|
738
|
+
function refusesEveryDomainScope(document, readable) {
|
|
739
|
+
return document.state !== void 0 && !allowsSomeDomainScope(document, readable);
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* The domain scopes the document does not hold whose read decision reads their
|
|
743
|
+
* own state. A condition's `doc.<scope>` path resolves only for that scope, so
|
|
744
|
+
* every other scope is decided on its name and the policy alone.
|
|
745
|
+
*/
|
|
746
|
+
function unheldScopesReadOnState(document) {
|
|
747
|
+
const grants = authOf(document)?.grants ?? [];
|
|
748
|
+
const onState = /* @__PURE__ */ new Set();
|
|
749
|
+
for (const grant of grants) {
|
|
750
|
+
collectStateScopes(grant.where, onState);
|
|
751
|
+
collectStateScopes(grant.principal, onState);
|
|
752
|
+
}
|
|
753
|
+
if (onState.size === 0) return [];
|
|
754
|
+
const held = document.state ?? {};
|
|
755
|
+
return domainScopesOf(document).filter((scope) => onState.has(scope) && !(scope in held));
|
|
756
|
+
}
|
|
757
|
+
function collectStateScopes(node, into) {
|
|
758
|
+
if (Array.isArray(node)) {
|
|
759
|
+
for (const child of node) collectStateScopes(child, into);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (typeof node !== "object" || node === null) return;
|
|
763
|
+
const { attr } = node;
|
|
764
|
+
if (typeof attr === "string") {
|
|
765
|
+
const [root, scope] = attr.split(".");
|
|
766
|
+
if (root === "doc" && scope) into.add(scope);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
for (const child of Object.values(node)) collectStateScopes(child, into);
|
|
770
|
+
}
|
|
725
771
|
/** What a gate does with an uninitialized policy when told nothing. */
|
|
726
772
|
const OPEN_BY_DEFAULT = { withholdUninitialized: false };
|
|
727
773
|
/**
|
|
@@ -784,8 +830,26 @@ var BareReadGate = class {
|
|
|
784
830
|
scope
|
|
785
831
|
}) === "allow");
|
|
786
832
|
}
|
|
833
|
+
prepare(document) {
|
|
834
|
+
return Promise.resolve((subject) => this.scopePredicate(document, subject));
|
|
835
|
+
}
|
|
787
836
|
};
|
|
788
837
|
/**
|
|
838
|
+
* Rethrows a failed read unless the document is confirmed not live. The read
|
|
839
|
+
* side reports absence as a plain Error, so absence is checked rather than
|
|
840
|
+
* inferred from the message, and a failed check surfaces the read's own error.
|
|
841
|
+
*/
|
|
842
|
+
async function assertAbsent(documentView, documentId, error, signal) {
|
|
843
|
+
if (DocumentNotFoundError.isError(error)) return;
|
|
844
|
+
let exists;
|
|
845
|
+
try {
|
|
846
|
+
exists = await documentView.exists([documentId], "LiveOnly", void 0, signal);
|
|
847
|
+
} catch {
|
|
848
|
+
throw error;
|
|
849
|
+
}
|
|
850
|
+
if (exists[0]) throw error;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
789
853
|
* Answers a stream read from the document already fetched, and anything else
|
|
790
854
|
* through the read side.
|
|
791
855
|
*
|
|
@@ -819,20 +883,10 @@ var SeededStateReader = class {
|
|
|
819
883
|
scopes: [scope]
|
|
820
884
|
}, void 0, signal);
|
|
821
885
|
} catch (error) {
|
|
822
|
-
await this.
|
|
886
|
+
await assertAbsent(this.documentView, documentId, error, signal);
|
|
823
887
|
throw new DocumentNotFoundError(documentId);
|
|
824
888
|
}
|
|
825
889
|
}
|
|
826
|
-
async assertAbsent(documentId, error, signal) {
|
|
827
|
-
if (DocumentNotFoundError.isError(error)) return;
|
|
828
|
-
let exists;
|
|
829
|
-
try {
|
|
830
|
-
exists = await this.documentView.exists([documentId], "LiveOnly", void 0, signal);
|
|
831
|
-
} catch {
|
|
832
|
-
throw error;
|
|
833
|
-
}
|
|
834
|
-
if (exists[0]) throw error;
|
|
835
|
-
}
|
|
836
890
|
};
|
|
837
891
|
/**
|
|
838
892
|
* Evaluates a read against the registered decision model, built at the stream
|
|
@@ -868,10 +922,18 @@ var ModelReadGate = class {
|
|
|
868
922
|
* group's other scopes are its own business and stay behind its own grants.
|
|
869
923
|
*/
|
|
870
924
|
async scopePredicate(document, subject, branch, signal) {
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
925
|
+
return (await this.prepare(document, branch, signal))(subject);
|
|
926
|
+
}
|
|
927
|
+
async prepare(document, branch, signal) {
|
|
928
|
+
const policy = await this.ownPolicy(document, branch, signal);
|
|
929
|
+
if (!this.servesGroup(document)) return (subject) => Promise.resolve(policy(subject));
|
|
930
|
+
const referencers = this.referencerReads(document.header.id, signal);
|
|
931
|
+
return async (subject) => {
|
|
932
|
+
const own = policy(subject);
|
|
933
|
+
if (own(GROUP_MEMBERSHIP_SCOPE)) return own;
|
|
934
|
+
const audience = await this.servesGroupTo(subject, referencers);
|
|
935
|
+
return (scope) => own(scope) || audience && scope === GROUP_MEMBERSHIP_SCOPE;
|
|
936
|
+
};
|
|
875
937
|
}
|
|
876
938
|
/**
|
|
877
939
|
* Whether the subject is served this group because a policy names it.
|
|
@@ -905,11 +967,8 @@ var ModelReadGate = class {
|
|
|
905
967
|
* running with nobody awaiting it, which is where unhandled rejections come
|
|
906
968
|
* from.
|
|
907
969
|
*/
|
|
908
|
-
async servesGroupTo(
|
|
909
|
-
|
|
910
|
-
const referencers = await this.operationIndex.getGroupReferencers(groupId, signal);
|
|
911
|
-
const examined = referencers.slice(0, MAX_EXAMINED_REFERENCERS);
|
|
912
|
-
if (examined.length < referencers.length) this.logger?.warn(`Group ${groupId} is referenced by ${referencers.length} documents; only the first ${MAX_EXAMINED_REFERENCERS} decide whether it is served`);
|
|
970
|
+
async servesGroupTo(subject, referencers) {
|
|
971
|
+
const examined = await referencers.examined();
|
|
913
972
|
const walk = {
|
|
914
973
|
next: 0,
|
|
915
974
|
served: false,
|
|
@@ -920,7 +979,7 @@ var ModelReadGate = class {
|
|
|
920
979
|
const referencerId = examined[walk.next++];
|
|
921
980
|
let serves = false;
|
|
922
981
|
try {
|
|
923
|
-
serves = await
|
|
982
|
+
serves = (await referencers.serves(referencerId))(subject);
|
|
924
983
|
} catch (error) {
|
|
925
984
|
walk.failure ??= Error.isError(error) ? error : /* @__PURE__ */ new Error(`Probing referencer ${referencerId} failed`);
|
|
926
985
|
}
|
|
@@ -931,21 +990,44 @@ var ModelReadGate = class {
|
|
|
931
990
|
if (!walk.served && walk.failure) throw walk.failure;
|
|
932
991
|
return walk.served;
|
|
933
992
|
}
|
|
993
|
+
/** Each read made once, however many subjects the walk runs for. */
|
|
994
|
+
referencerReads(groupId, signal) {
|
|
995
|
+
let examined;
|
|
996
|
+
const serves = /* @__PURE__ */ new Map();
|
|
997
|
+
return {
|
|
998
|
+
examined: () => examined ??= this.examinedReferencers(groupId, signal),
|
|
999
|
+
serves: (referencerId) => {
|
|
1000
|
+
let read = serves.get(referencerId);
|
|
1001
|
+
if (!read) {
|
|
1002
|
+
read = this.servesThrough(referencerId, signal);
|
|
1003
|
+
serves.set(referencerId, read);
|
|
1004
|
+
}
|
|
1005
|
+
return read;
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
async examinedReferencers(groupId, signal) {
|
|
1010
|
+
if (!this.operationIndex) return [];
|
|
1011
|
+
const referencers = await this.operationIndex.getGroupReferencers(groupId, signal);
|
|
1012
|
+
const examined = referencers.slice(0, MAX_EXAMINED_REFERENCERS);
|
|
1013
|
+
if (examined.length < referencers.length) this.logger?.warn(`Group ${groupId} is referenced by ${referencers.length} documents; only the first ${MAX_EXAMINED_REFERENCERS} decide whether it is served`);
|
|
1014
|
+
return examined;
|
|
1015
|
+
}
|
|
934
1016
|
/**
|
|
935
|
-
* Whether one referencing document serves
|
|
1017
|
+
* Whether one referencing document serves a subject any domain scope. A
|
|
936
1018
|
* referencer this replica does not hold serves nothing, which fails closed
|
|
937
1019
|
* the same way a group it does not hold does.
|
|
938
1020
|
*/
|
|
939
|
-
async servesThrough(referencerId,
|
|
1021
|
+
async servesThrough(referencerId, signal) {
|
|
940
1022
|
let referencer;
|
|
941
1023
|
try {
|
|
942
1024
|
referencer = await this.documentView.get(referencerId, { branch: GROUP_BRANCH }, void 0, signal);
|
|
943
1025
|
} catch {
|
|
944
|
-
return false;
|
|
1026
|
+
return () => false;
|
|
945
1027
|
}
|
|
946
|
-
if (this.servesGroup(referencer)) return false;
|
|
947
|
-
const
|
|
948
|
-
return
|
|
1028
|
+
if (this.servesGroup(referencer)) return () => false;
|
|
1029
|
+
const policy = await this.ownPolicy(referencer, GROUP_BRANCH, signal);
|
|
1030
|
+
return (subject) => allowsSomeDomainScope(referencer, policy(subject));
|
|
949
1031
|
}
|
|
950
1032
|
servesGroup(document) {
|
|
951
1033
|
return this.servesGroups && this.operationIndex !== void 0 && document.header.documentType === groupDocumentType;
|
|
@@ -963,9 +1045,12 @@ var ModelReadGate = class {
|
|
|
963
1045
|
* publish an unpoliced group it names: the referencer walk asks this question
|
|
964
1046
|
* of the referencing document, whose real policy answers it.
|
|
965
1047
|
*/
|
|
966
|
-
async
|
|
1048
|
+
async ownPolicy(document, branch, signal) {
|
|
967
1049
|
const auth = authOf(document);
|
|
968
|
-
if (!auth || !auth.version)
|
|
1050
|
+
if (!auth || !auth.version) {
|
|
1051
|
+
const readable = this.options.withholdUninitialized ? (scope) => ALWAYS_READABLE_SCOPES.has(scope) : () => true;
|
|
1052
|
+
return () => readable;
|
|
1053
|
+
}
|
|
969
1054
|
const target = {
|
|
970
1055
|
documentId: document.header.id,
|
|
971
1056
|
branch
|
|
@@ -973,7 +1058,7 @@ var ModelReadGate = class {
|
|
|
973
1058
|
const built = await buildDecisionModel(new SeededStateReader(this.documentView, document, branch), this.model, target, signal);
|
|
974
1059
|
const definition = this.model(target);
|
|
975
1060
|
const scopeStates = document.state ?? {};
|
|
976
|
-
return (scope) => ALWAYS_READABLE_SCOPES.has(scope) || definition.decide(built.model, subject, {
|
|
1061
|
+
return (subject) => (scope) => ALWAYS_READABLE_SCOPES.has(scope) || definition.decide(built.model, subject, {
|
|
977
1062
|
verb: "read",
|
|
978
1063
|
scope
|
|
979
1064
|
}, {
|
|
@@ -983,6 +1068,78 @@ var ModelReadGate = class {
|
|
|
983
1068
|
}
|
|
984
1069
|
};
|
|
985
1070
|
//#endregion
|
|
1071
|
+
//#region src/client/event-reads.ts
|
|
1072
|
+
/** One change event's reads, shared by every subscription it reaches. */
|
|
1073
|
+
var EventReads = class {
|
|
1074
|
+
reactor;
|
|
1075
|
+
documentView;
|
|
1076
|
+
readGate;
|
|
1077
|
+
documents = /* @__PURE__ */ new Map();
|
|
1078
|
+
existence = /* @__PURE__ */ new Map();
|
|
1079
|
+
prepared = /* @__PURE__ */ new WeakMap();
|
|
1080
|
+
constructor(reactor, documentView, readGate) {
|
|
1081
|
+
this.reactor = reactor;
|
|
1082
|
+
this.documentView = documentView;
|
|
1083
|
+
this.readGate = readGate;
|
|
1084
|
+
}
|
|
1085
|
+
get(id, view) {
|
|
1086
|
+
const key = JSON.stringify([
|
|
1087
|
+
id,
|
|
1088
|
+
view?.branch,
|
|
1089
|
+
view?.scopes
|
|
1090
|
+
]);
|
|
1091
|
+
let read = this.documents.get(key);
|
|
1092
|
+
if (!read) {
|
|
1093
|
+
read = this.reactor.get(id, view);
|
|
1094
|
+
this.documents.set(key, read);
|
|
1095
|
+
}
|
|
1096
|
+
return read;
|
|
1097
|
+
}
|
|
1098
|
+
exists(ids, existence) {
|
|
1099
|
+
const key = JSON.stringify([ids, existence]);
|
|
1100
|
+
let read = this.existence.get(key);
|
|
1101
|
+
if (!read) {
|
|
1102
|
+
read = this.documentView.exists(ids, existence);
|
|
1103
|
+
this.existence.set(key, read);
|
|
1104
|
+
}
|
|
1105
|
+
return read;
|
|
1106
|
+
}
|
|
1107
|
+
async scopePredicate(document, subject, branch) {
|
|
1108
|
+
if (!this.readGate.prepare) return this.readGate.scopePredicate(document, subject, branch);
|
|
1109
|
+
let byBranch = this.prepared.get(document);
|
|
1110
|
+
if (!byBranch) {
|
|
1111
|
+
byBranch = /* @__PURE__ */ new Map();
|
|
1112
|
+
this.prepared.set(document, byBranch);
|
|
1113
|
+
}
|
|
1114
|
+
let prepared = byBranch.get(branch);
|
|
1115
|
+
if (!prepared) {
|
|
1116
|
+
prepared = this.readGate.prepare(document, branch);
|
|
1117
|
+
byBranch.set(branch, prepared);
|
|
1118
|
+
}
|
|
1119
|
+
return (await prepared)(subject);
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
var EventReadsSource = class {
|
|
1123
|
+
reactor;
|
|
1124
|
+
documentView;
|
|
1125
|
+
readGate;
|
|
1126
|
+
current;
|
|
1127
|
+
constructor(reactor, documentView, readGate) {
|
|
1128
|
+
this.reactor = reactor;
|
|
1129
|
+
this.documentView = documentView;
|
|
1130
|
+
this.readGate = readGate;
|
|
1131
|
+
}
|
|
1132
|
+
forEvent() {
|
|
1133
|
+
if (!this.current) {
|
|
1134
|
+
this.current = new EventReads(this.reactor, this.documentView, this.readGate);
|
|
1135
|
+
queueMicrotask(() => {
|
|
1136
|
+
this.current = void 0;
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
return this.current;
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
//#endregion
|
|
986
1143
|
//#region src/client/util.ts
|
|
987
1144
|
function authSubjectFromSigner(signer) {
|
|
988
1145
|
return {
|
|
@@ -997,6 +1154,19 @@ function withAuthScope(view) {
|
|
|
997
1154
|
};
|
|
998
1155
|
return view;
|
|
999
1156
|
}
|
|
1157
|
+
function withAllScopes(view) {
|
|
1158
|
+
if (!view?.scopes) return view;
|
|
1159
|
+
const { scopes: _narrowed, ...whole } = view;
|
|
1160
|
+
return whole;
|
|
1161
|
+
}
|
|
1162
|
+
function narrowedScopes(view) {
|
|
1163
|
+
if (!view?.scopes || view.scopes.length === 0) return;
|
|
1164
|
+
return /* @__PURE__ */ new Set([
|
|
1165
|
+
"document",
|
|
1166
|
+
"auth",
|
|
1167
|
+
...view.scopes
|
|
1168
|
+
]);
|
|
1169
|
+
}
|
|
1000
1170
|
/**
|
|
1001
1171
|
* Drops the scopes the predicate refuses. The predicate is resolved by the read
|
|
1002
1172
|
* gate, which decides against the whole policy once per document; this only
|
|
@@ -1069,6 +1239,7 @@ var ReactorClient = class {
|
|
|
1069
1239
|
documentIndexer;
|
|
1070
1240
|
documentView;
|
|
1071
1241
|
readGate;
|
|
1242
|
+
eventReads;
|
|
1072
1243
|
actionEvaluation;
|
|
1073
1244
|
createSignaturePolicy;
|
|
1074
1245
|
drives;
|
|
@@ -1081,33 +1252,12 @@ var ReactorClient = class {
|
|
|
1081
1252
|
this.documentIndexer = documentIndexer;
|
|
1082
1253
|
this.documentView = documentView;
|
|
1083
1254
|
this.readGate = readGate;
|
|
1255
|
+
this.eventReads = new EventReadsSource(reactor, documentView, readGate);
|
|
1084
1256
|
this.actionEvaluation = actionEvaluation;
|
|
1085
1257
|
this.createSignaturePolicy = createSignaturePolicy;
|
|
1086
1258
|
this.drives = new DriveClient(this, logger, reactor, signer);
|
|
1087
1259
|
this.logger.verbose("ReactorClient initialized");
|
|
1088
1260
|
}
|
|
1089
|
-
readSubject(subject) {
|
|
1090
|
-
return subject ?? authSubjectFromSigner(this.signer);
|
|
1091
|
-
}
|
|
1092
|
-
/**
|
|
1093
|
-
* Which scopes of one document the subject may read. Resolved once per
|
|
1094
|
-
* document, so the gate builds its model once however many scopes are then
|
|
1095
|
-
* tested, and the filtering itself stays synchronous.
|
|
1096
|
-
*/
|
|
1097
|
-
readableScopes(document, view, signal) {
|
|
1098
|
-
return this.readGate.scopePredicate(document, this.readSubject(view?.subject), view?.branch ?? "main", signal);
|
|
1099
|
-
}
|
|
1100
|
-
/**
|
|
1101
|
-
* One document, filtered to the scopes the subject may read. Every method
|
|
1102
|
-
* that hands a document back goes through here, including the ones that
|
|
1103
|
-
* follow a write: a document returned from a mutation is a read like any
|
|
1104
|
-
* other, and returning it whole served scopes the same subject would be
|
|
1105
|
-
* refused by `get`. Its author still sees what it wrote, because an allow on
|
|
1106
|
-
* execute confers read of that scope.
|
|
1107
|
-
*/
|
|
1108
|
-
async gateDocument(document, view, signal) {
|
|
1109
|
-
return filterReadableScopes(document, await this.readableScopes(document, view, signal));
|
|
1110
|
-
}
|
|
1111
1261
|
/**
|
|
1112
1262
|
* Retrieves a list of document model modules.
|
|
1113
1263
|
*/
|
|
@@ -1213,31 +1363,6 @@ var ReactorClient = class {
|
|
|
1213
1363
|
nextCursor: Object.keys(activeCursors).length > 0 ? encodeCompositeCursor(activeCursors) : void 0
|
|
1214
1364
|
};
|
|
1215
1365
|
}
|
|
1216
|
-
async getOperationsWithCompositeCursor(documentId, view, filter, paging, signal, canRead) {
|
|
1217
|
-
const scopeCursors = decodeCompositeCursor(paging.cursor);
|
|
1218
|
-
const allOperations = [];
|
|
1219
|
-
const activeCursors = {};
|
|
1220
|
-
for (const [scopeName, cursor] of Object.entries(scopeCursors)) {
|
|
1221
|
-
if (!canRead(scopeName)) continue;
|
|
1222
|
-
const scopeView = {
|
|
1223
|
-
...view,
|
|
1224
|
-
scopes: [scopeName]
|
|
1225
|
-
};
|
|
1226
|
-
const scopePaging = {
|
|
1227
|
-
cursor,
|
|
1228
|
-
limit: paging.limit
|
|
1229
|
-
};
|
|
1230
|
-
const scopeResult = (await this.reactor.getOperations(documentId, scopeView, filter, scopePaging, void 0, signal))[scopeName];
|
|
1231
|
-
allOperations.push(...scopeResult.results);
|
|
1232
|
-
if (scopeResult.nextCursor) activeCursors[scopeName] = scopeResult.nextCursor;
|
|
1233
|
-
}
|
|
1234
|
-
allOperations.sort((a, b) => a.index - b.index);
|
|
1235
|
-
return {
|
|
1236
|
-
results: allOperations,
|
|
1237
|
-
options: paging,
|
|
1238
|
-
nextCursor: Object.keys(activeCursors).length > 0 ? encodeCompositeCursor(activeCursors) : void 0
|
|
1239
|
-
};
|
|
1240
|
-
}
|
|
1241
1366
|
/**
|
|
1242
1367
|
* Retrieves outgoing relationships of a given type from a source document.
|
|
1243
1368
|
*/
|
|
@@ -1291,68 +1416,24 @@ var ReactorClient = class {
|
|
|
1291
1416
|
return this.gateEdges(edges, "sourceId", view, signal);
|
|
1292
1417
|
}
|
|
1293
1418
|
/**
|
|
1294
|
-
* Drops the edges whose far-end document the subject may read no domain scope
|
|
1295
|
-
* of. An edge is withheld whole rather than stripped of its metadata: the
|
|
1296
|
-
* document-shaped relationship reads already answer with the far end stripped
|
|
1297
|
-
* to the scopes the gate allows, so the far end's existence is disclosed
|
|
1298
|
-
* either way, but an edge's metadata is content about the pair that the far
|
|
1299
|
-
* end's own reads would refuse. An edge to a far end stripped to nothing
|
|
1300
|
-
* therefore carries content past a refusal, and there is no useful shell to
|
|
1301
|
-
* hand back in its place.
|
|
1302
|
-
*
|
|
1303
|
-
* `nextCursor` and `options` are left as the underlying stream reported them,
|
|
1304
|
-
* because a caller must feed them back to resume from the right position. A
|
|
1305
|
-
* gated page can therefore be shorter than the limit it asked for.
|
|
1306
|
-
*/
|
|
1307
|
-
async gateEdges(page, farEnd, view, signal) {
|
|
1308
|
-
const ids = [...new Set(page.results.map((edge) => edge[farEnd]))];
|
|
1309
|
-
if (ids.length === 0) return page;
|
|
1310
|
-
const farEndView = {
|
|
1311
|
-
subject: view?.subject,
|
|
1312
|
-
branch: view?.branch
|
|
1313
|
-
};
|
|
1314
|
-
const documents = await this.reactor.find({ ids }, farEndView, {
|
|
1315
|
-
cursor: "0",
|
|
1316
|
-
limit: ids.length
|
|
1317
|
-
}, void 0, signal);
|
|
1318
|
-
const readable = new Map(await Promise.all(documents.results.map(async (doc) => {
|
|
1319
|
-
const allows = await this.readableScopes(doc, view, signal);
|
|
1320
|
-
const domainScopes = Object.keys(doc.state).filter((scope) => !ALWAYS_READABLE_SCOPES.has(scope));
|
|
1321
|
-
return [doc.header.id, domainScopes.length === 0 || domainScopes.some(allows)];
|
|
1322
|
-
})));
|
|
1323
|
-
const results = page.results.filter((edge) => readable.get(edge[farEnd]) !== false);
|
|
1324
|
-
if (results.length === page.results.length) return page;
|
|
1325
|
-
const nextPage = page.next;
|
|
1326
|
-
return {
|
|
1327
|
-
...page,
|
|
1328
|
-
results,
|
|
1329
|
-
next: nextPage ? async () => this.gateEdges(await nextPage(), farEnd, view, signal) : void 0
|
|
1330
|
-
};
|
|
1331
|
-
}
|
|
1332
|
-
/**
|
|
1333
|
-
* One relationship edge, or undefined when it does not exist. A point lookup:
|
|
1334
|
-
* the pair is filtered in SQL rather than scanned out of the source's edge
|
|
1335
|
-
* list, which on a drive with thousands of children is the difference between
|
|
1336
|
-
* one query and dozens.
|
|
1337
|
-
*/
|
|
1338
|
-
async readRelationshipEdge(sourceIdentifier, targetIdentifier, relationshipType, view, signal) {
|
|
1339
|
-
const sourceId = await this.documentView.resolveIdOrSlug(sourceIdentifier, view, void 0, signal);
|
|
1340
|
-
const targetId = await this.documentView.resolveIdOrSlug(targetIdentifier, view, void 0, signal);
|
|
1341
|
-
return (await this.documentIndexer.getDirectedRelationships(sourceId, targetId, [relationshipType], {
|
|
1342
|
-
cursor: "0",
|
|
1343
|
-
limit: 1
|
|
1344
|
-
}, void 0, signal)).results[0];
|
|
1345
|
-
}
|
|
1346
|
-
/**
|
|
1347
1419
|
* Filters documents by criteria and returns a list of them
|
|
1348
1420
|
*/
|
|
1349
1421
|
async find(search, view, paging, signal) {
|
|
1350
1422
|
this.logger.verbose("find(@search, @view, @paging)", search, view, paging);
|
|
1351
1423
|
const results = await this.reactor.find(search, withAuthScope(view), paging, void 0, signal);
|
|
1352
|
-
return
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1424
|
+
return this.gateListing(results, view, signal);
|
|
1425
|
+
}
|
|
1426
|
+
/** Whether `find` would serve the document; false when it is absent. */
|
|
1427
|
+
async isServed(identifier, view, signal) {
|
|
1428
|
+
this.logger.verbose("isServed(@identifier, @view)", identifier, view);
|
|
1429
|
+
let document;
|
|
1430
|
+
try {
|
|
1431
|
+
document = await this.reactor.getByIdOrSlug(identifier, withAuthScope(view), void 0, signal);
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
await assertAbsent(this.documentView, identifier, error, signal);
|
|
1434
|
+
return false;
|
|
1435
|
+
}
|
|
1436
|
+
return await this.gateListed(document, view, signal) !== void 0;
|
|
1356
1437
|
}
|
|
1357
1438
|
/**
|
|
1358
1439
|
* Predicts the admission verdict for each candidate. See
|
|
@@ -1393,39 +1474,6 @@ var ReactorClient = class {
|
|
|
1393
1474
|
anyDenied: allowed < candidates.length
|
|
1394
1475
|
};
|
|
1395
1476
|
}
|
|
1396
|
-
/**
|
|
1397
|
-
* The decision model for one target document, built at its stream heads.
|
|
1398
|
-
*
|
|
1399
|
-
* The document is fetched unfiltered, because the policy is what decides:
|
|
1400
|
-
* reading it through the read gate would withhold the very scopes the
|
|
1401
|
-
* decision is about. A deleted document is served at its deletion boundary,
|
|
1402
|
-
* which is what lets the model refuse an execute against it -- authEnforcement
|
|
1403
|
-
* requires documentDecisions, so that read is available whenever this runs.
|
|
1404
|
-
*
|
|
1405
|
-
* Reading past the gate discloses nothing a submit does not. The `auth` and
|
|
1406
|
-
* `document` scopes are readable by every holder, so a verdict resting on the
|
|
1407
|
-
* policy alone is one the caller could compute unaided; and a verdict resting
|
|
1408
|
-
* on a conditional grant reads the executing scope's state exactly as
|
|
1409
|
-
* admission reads it, so the answer here is what submitting and being refused
|
|
1410
|
-
* would have revealed anyway.
|
|
1411
|
-
*
|
|
1412
|
-
* The append condition the build records is dropped. It guards a write, and
|
|
1413
|
-
* this makes none; reproducing it is also what the preflight cannot do, which
|
|
1414
|
-
* is why the answer is a prediction.
|
|
1415
|
-
*/
|
|
1416
|
-
async buildEvaluationTarget(config, documentId, branch, signal) {
|
|
1417
|
-
const document = await this.documentView.get(documentId, { branch }, void 0, signal);
|
|
1418
|
-
const target = {
|
|
1419
|
-
documentId,
|
|
1420
|
-
branch
|
|
1421
|
-
};
|
|
1422
|
-
const built = await buildDecisionModel(new SeededStateReader(this.documentView, document, branch), config.model, target, signal);
|
|
1423
|
-
return {
|
|
1424
|
-
definition: config.model(target),
|
|
1425
|
-
model: built.model,
|
|
1426
|
-
scopeStates: document.state ?? {}
|
|
1427
|
-
};
|
|
1428
|
-
}
|
|
1429
1477
|
getCreateSignaturePolicy() {
|
|
1430
1478
|
return Promise.resolve(this.createSignaturePolicy);
|
|
1431
1479
|
}
|
|
@@ -1608,18 +1656,6 @@ var ReactorClient = class {
|
|
|
1608
1656
|
}, signal);
|
|
1609
1657
|
return this.reactor.execute(documentId, branch, signedActions, signal);
|
|
1610
1658
|
}
|
|
1611
|
-
/**
|
|
1612
|
-
* The id a write on `identifier` is stored under, which its signatures bind.
|
|
1613
|
-
* A create names its own id, and an id no slug maps to is taken as given, so
|
|
1614
|
-
* a document still in flight resolves to itself.
|
|
1615
|
-
*/
|
|
1616
|
-
async resolveWriteTarget(identifier, branch, actions, signal) {
|
|
1617
|
-
if (actions.some((action) => action.type === "CREATE_DOCUMENT")) return identifier;
|
|
1618
|
-
const view = { branch };
|
|
1619
|
-
const bySlug = await this.documentView.resolveSlug(identifier, view, void 0, signal);
|
|
1620
|
-
if (bySlug === void 0 || bySlug === identifier) return identifier;
|
|
1621
|
-
return this.documentView.resolveIdOrSlug(identifier, view, void 0, signal);
|
|
1622
|
-
}
|
|
1623
1659
|
async executeBatch(request, signal) {
|
|
1624
1660
|
this.logger.verbose("executeBatch(@count jobs)", request.jobs.length);
|
|
1625
1661
|
const signedJobs = await Promise.all(request.jobs.map(async (job) => {
|
|
@@ -1776,7 +1812,13 @@ var ReactorClient = class {
|
|
|
1776
1812
|
*/
|
|
1777
1813
|
subscribe(search, callback, view) {
|
|
1778
1814
|
this.logger.verbose("subscribe(@search, @view)", search, view);
|
|
1779
|
-
const
|
|
1815
|
+
const served = async (type, documents, reads) => {
|
|
1816
|
+
const documentsServed = await this.gateServedAll(documents, view, void 0, reads);
|
|
1817
|
+
return documentsServed.length > 0 ? {
|
|
1818
|
+
type,
|
|
1819
|
+
documents: documentsServed
|
|
1820
|
+
} : void 0;
|
|
1821
|
+
};
|
|
1780
1822
|
let disposed = false;
|
|
1781
1823
|
let delivering = Promise.resolve();
|
|
1782
1824
|
/**
|
|
@@ -1790,49 +1832,46 @@ var ReactorClient = class {
|
|
|
1790
1832
|
* gated is withheld, because serving it unfiltered would leak the scopes
|
|
1791
1833
|
* the gate did not clear -- but it is logged rather than swallowed, since
|
|
1792
1834
|
* the gate rethrows a transient failure precisely so it is not read as a
|
|
1793
|
-
* denial.
|
|
1835
|
+
* denial. An event that resolves to undefined was withheld.
|
|
1794
1836
|
*/
|
|
1795
1837
|
const deliver = (event) => {
|
|
1796
1838
|
Promise.resolve(event).catch(() => void 0);
|
|
1797
1839
|
delivering = delivering.then(async () => {
|
|
1798
1840
|
const built = await event;
|
|
1799
|
-
if (disposed) return;
|
|
1841
|
+
if (disposed || !built) return;
|
|
1800
1842
|
callback(built);
|
|
1801
1843
|
}).catch((error) => {
|
|
1802
1844
|
this.logger.error("Subscription delivery failed for @search: @Error", { search }, error);
|
|
1803
1845
|
});
|
|
1804
1846
|
};
|
|
1805
1847
|
const unsubscribeCreated = this.subscriptionManager.onDocumentCreated((result) => {
|
|
1848
|
+
const reads = this.eventReads.forEvent();
|
|
1806
1849
|
deliver((async () => {
|
|
1807
|
-
const documents = await Promise.all(result.results.map((id) =>
|
|
1808
|
-
return
|
|
1809
|
-
type: "created",
|
|
1810
|
-
documents: await Promise.all(documents.map(readable))
|
|
1811
|
-
};
|
|
1850
|
+
const documents = await Promise.all(result.results.map((id) => reads.get(id, withAllScopes(view))));
|
|
1851
|
+
return served("created", documents, reads);
|
|
1812
1852
|
})());
|
|
1813
1853
|
}, search);
|
|
1814
1854
|
const unsubscribeDeleted = this.subscriptionManager.onDocumentDeleted((documentIds) => {
|
|
1815
|
-
|
|
1855
|
+
const reads = this.eventReads.forEvent();
|
|
1856
|
+
for (const childId of documentIds) deliver((async () => await this.servesEvery([childId], view, reads) ? {
|
|
1816
1857
|
type: "deleted",
|
|
1817
1858
|
documents: [],
|
|
1818
|
-
context: { childId
|
|
1819
|
-
});
|
|
1859
|
+
context: { childId }
|
|
1860
|
+
} : void 0)());
|
|
1820
1861
|
}, search);
|
|
1821
1862
|
const unsubscribeUpdated = this.subscriptionManager.onDocumentStateUpdated((result) => {
|
|
1822
|
-
deliver((
|
|
1823
|
-
type: "updated",
|
|
1824
|
-
documents: await Promise.all(result.results.map(readable))
|
|
1825
|
-
}))());
|
|
1863
|
+
deliver(served("updated", result.results, this.eventReads.forEvent()));
|
|
1826
1864
|
}, search, view);
|
|
1827
1865
|
const unsubscribeRelationship = this.subscriptionManager.onRelationshipChanged((parentId, childId, changeType) => {
|
|
1828
|
-
|
|
1866
|
+
const reads = this.eventReads.forEvent();
|
|
1867
|
+
deliver((async () => await this.servesEvery([parentId, childId], view, reads) ? {
|
|
1829
1868
|
type: changeType === "added" ? "child_added" : "child_removed",
|
|
1830
1869
|
documents: [],
|
|
1831
1870
|
context: {
|
|
1832
1871
|
parentId,
|
|
1833
1872
|
childId
|
|
1834
1873
|
}
|
|
1835
|
-
});
|
|
1874
|
+
} : void 0)());
|
|
1836
1875
|
}, search);
|
|
1837
1876
|
return () => {
|
|
1838
1877
|
disposed = true;
|
|
@@ -1842,6 +1881,225 @@ var ReactorClient = class {
|
|
|
1842
1881
|
unsubscribeRelationship();
|
|
1843
1882
|
};
|
|
1844
1883
|
}
|
|
1884
|
+
readSubject(subject) {
|
|
1885
|
+
return subject ?? authSubjectFromSigner(this.signer);
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Which scopes of one document the subject may read. Resolved once per
|
|
1889
|
+
* document, so the gate builds its model once however many scopes are then
|
|
1890
|
+
* tested, and the filtering itself stays synchronous.
|
|
1891
|
+
*/
|
|
1892
|
+
readableScopes(document, view, signal, reads) {
|
|
1893
|
+
const subject = this.readSubject(view?.subject);
|
|
1894
|
+
const branch = view?.branch ?? "main";
|
|
1895
|
+
return reads ? reads.scopePredicate(document, subject, branch) : this.readGate.scopePredicate(document, subject, branch, signal);
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* One document, filtered to the scopes the subject may read. Every method
|
|
1899
|
+
* that hands a document back goes through here, including the ones that
|
|
1900
|
+
* follow a write: a document returned from a mutation is a read like any
|
|
1901
|
+
* other, and returning it whole served scopes the same subject would be
|
|
1902
|
+
* refused by `get`. Its author still sees what it wrote, because an allow on
|
|
1903
|
+
* execute confers read of that scope.
|
|
1904
|
+
*/
|
|
1905
|
+
async gateDocument(document, view, signal) {
|
|
1906
|
+
return filterReadableScopes(document, await this.readableScopes(document, view, signal));
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* One document as a listing or a feed serves it, or undefined when withheld.
|
|
1910
|
+
* The document must hold every scope, or withholding is decided on a subset;
|
|
1911
|
+
* a narrowing view is applied here instead.
|
|
1912
|
+
*/
|
|
1913
|
+
async gateServed(document, view, signal, reads) {
|
|
1914
|
+
const readable = await this.readableScopes(document, view, signal, reads);
|
|
1915
|
+
if (refusesEveryDomainScope(document, readable)) return;
|
|
1916
|
+
const narrowed = narrowedScopes(view);
|
|
1917
|
+
return filterReadableScopes(document, narrowed ? (scope) => readable(scope) && narrowed.has(scope) : readable);
|
|
1918
|
+
}
|
|
1919
|
+
async gateServedAll(documents, view, signal, reads) {
|
|
1920
|
+
return (await Promise.all(documents.map((document) => this.gateServed(document, view, signal, reads)))).filter((document) => document !== void 0);
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* Gates a page and every page after it. `nextCursor` is left as the stream
|
|
1924
|
+
* reported it.
|
|
1925
|
+
*/
|
|
1926
|
+
async gateListing(page, view, signal) {
|
|
1927
|
+
const results = (await Promise.all(page.results.map((document) => this.gateListed(document, view, signal)))).filter((document) => document !== void 0);
|
|
1928
|
+
const next = page.next;
|
|
1929
|
+
return {
|
|
1930
|
+
...page,
|
|
1931
|
+
results,
|
|
1932
|
+
next: next ? async () => this.gateListing(await next(), view, signal) : void 0
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* One document as a listing serves it, or undefined when withheld. It holds
|
|
1937
|
+
* only the view's scopes; a domain scope it lacks is decided on its name, or
|
|
1938
|
+
* on its state read now when a condition needs that.
|
|
1939
|
+
*/
|
|
1940
|
+
async gateListed(document, view, signal) {
|
|
1941
|
+
const readable = await this.readableScopes(document, view, signal);
|
|
1942
|
+
const unheld = unheldScopesReadOnState(document);
|
|
1943
|
+
if (refusesEveryDomainScope(document, unheld.length === 0 ? readable : (scope) => !unheld.includes(scope) && readable(scope)) && !await this.servesUnheld(document, unheld, view, signal)) return;
|
|
1944
|
+
return filterReadableScopes(document, readable);
|
|
1945
|
+
}
|
|
1946
|
+
async servesUnheld(document, scopes, view, signal) {
|
|
1947
|
+
if (scopes.length === 0) return false;
|
|
1948
|
+
const id = document.header.id;
|
|
1949
|
+
let held;
|
|
1950
|
+
try {
|
|
1951
|
+
held = await this.reactor.get(id, withAuthScope({
|
|
1952
|
+
...view,
|
|
1953
|
+
scopes
|
|
1954
|
+
}), void 0, signal);
|
|
1955
|
+
} catch (error) {
|
|
1956
|
+
await assertAbsent(this.documentView, id, error, signal);
|
|
1957
|
+
return false;
|
|
1958
|
+
}
|
|
1959
|
+
const readable = await this.readableScopes(held, view, signal);
|
|
1960
|
+
return scopes.some(readable);
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Whether an event naming only these ids may reach the subject. An id with no
|
|
1964
|
+
* live document behind it has no content left to protect, so it does not
|
|
1965
|
+
* withhold; a deleted document is read at its deletion boundary when
|
|
1966
|
+
* deletion is positional, and gated like any other.
|
|
1967
|
+
*/
|
|
1968
|
+
async servesEvery(ids, view, reads) {
|
|
1969
|
+
return (await Promise.all(ids.map((id) => this.servesId(id, view, reads)))).every(Boolean);
|
|
1970
|
+
}
|
|
1971
|
+
async servesId(id, view, reads) {
|
|
1972
|
+
let document;
|
|
1973
|
+
try {
|
|
1974
|
+
document = await reads.get(id, withAllScopes(view));
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
await assertAbsent(reads, id, error);
|
|
1977
|
+
return true;
|
|
1978
|
+
}
|
|
1979
|
+
return !refusesEveryDomainScope(document, await this.readableScopes(document, view, void 0, reads));
|
|
1980
|
+
}
|
|
1981
|
+
async getOperationsWithCompositeCursor(documentId, view, filter, paging, signal, canRead) {
|
|
1982
|
+
const scopeCursors = decodeCompositeCursor(paging.cursor);
|
|
1983
|
+
const allOperations = [];
|
|
1984
|
+
const activeCursors = {};
|
|
1985
|
+
for (const [scopeName, cursor] of Object.entries(scopeCursors)) {
|
|
1986
|
+
if (!canRead(scopeName)) continue;
|
|
1987
|
+
const scopeView = {
|
|
1988
|
+
...view,
|
|
1989
|
+
scopes: [scopeName]
|
|
1990
|
+
};
|
|
1991
|
+
const scopePaging = {
|
|
1992
|
+
cursor,
|
|
1993
|
+
limit: paging.limit
|
|
1994
|
+
};
|
|
1995
|
+
const scopeResult = (await this.reactor.getOperations(documentId, scopeView, filter, scopePaging, void 0, signal))[scopeName];
|
|
1996
|
+
allOperations.push(...scopeResult.results);
|
|
1997
|
+
if (scopeResult.nextCursor) activeCursors[scopeName] = scopeResult.nextCursor;
|
|
1998
|
+
}
|
|
1999
|
+
allOperations.sort((a, b) => a.index - b.index);
|
|
2000
|
+
return {
|
|
2001
|
+
results: allOperations,
|
|
2002
|
+
options: paging,
|
|
2003
|
+
nextCursor: Object.keys(activeCursors).length > 0 ? encodeCompositeCursor(activeCursors) : void 0
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
/**
|
|
2007
|
+
* Drops the edges whose far-end document the subject may read no domain scope
|
|
2008
|
+
* of. An edge is withheld whole rather than stripped of its metadata: the
|
|
2009
|
+
* document-shaped relationship reads already answer with the far end stripped
|
|
2010
|
+
* to the scopes the gate allows, so the far end's existence is disclosed
|
|
2011
|
+
* either way, but an edge's metadata is content about the pair that the far
|
|
2012
|
+
* end's own reads would refuse. An edge to a far end stripped to nothing
|
|
2013
|
+
* therefore carries content past a refusal, and there is no useful shell to
|
|
2014
|
+
* hand back in its place.
|
|
2015
|
+
*
|
|
2016
|
+
* `nextCursor` and `options` are left as the underlying stream reported them,
|
|
2017
|
+
* because a caller must feed them back to resume from the right position. A
|
|
2018
|
+
* gated page can therefore be shorter than the limit it asked for.
|
|
2019
|
+
*/
|
|
2020
|
+
async gateEdges(page, farEnd, view, signal) {
|
|
2021
|
+
const ids = [...new Set(page.results.map((edge) => edge[farEnd]))];
|
|
2022
|
+
if (ids.length === 0) return page;
|
|
2023
|
+
const farEndView = {
|
|
2024
|
+
subject: view?.subject,
|
|
2025
|
+
branch: view?.branch
|
|
2026
|
+
};
|
|
2027
|
+
const documents = await this.reactor.find({ ids }, farEndView, {
|
|
2028
|
+
cursor: "0",
|
|
2029
|
+
limit: ids.length
|
|
2030
|
+
}, void 0, signal);
|
|
2031
|
+
const readable = new Map(await Promise.all(documents.results.map(async (doc) => {
|
|
2032
|
+
const allows = await this.readableScopes(doc, view, signal);
|
|
2033
|
+
return [doc.header.id, !refusesEveryDomainScope(doc, allows)];
|
|
2034
|
+
})));
|
|
2035
|
+
const results = page.results.filter((edge) => readable.get(edge[farEnd]) !== false);
|
|
2036
|
+
if (results.length === page.results.length) return page;
|
|
2037
|
+
const nextPage = page.next;
|
|
2038
|
+
return {
|
|
2039
|
+
...page,
|
|
2040
|
+
results,
|
|
2041
|
+
next: nextPage ? async () => this.gateEdges(await nextPage(), farEnd, view, signal) : void 0
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
/**
|
|
2045
|
+
* One relationship edge, or undefined when it does not exist. A point lookup:
|
|
2046
|
+
* the pair is filtered in SQL rather than scanned out of the source's edge
|
|
2047
|
+
* list, which on a drive with thousands of children is the difference between
|
|
2048
|
+
* one query and dozens.
|
|
2049
|
+
*/
|
|
2050
|
+
async readRelationshipEdge(sourceIdentifier, targetIdentifier, relationshipType, view, signal) {
|
|
2051
|
+
const sourceId = await this.documentView.resolveIdOrSlug(sourceIdentifier, view, void 0, signal);
|
|
2052
|
+
const targetId = await this.documentView.resolveIdOrSlug(targetIdentifier, view, void 0, signal);
|
|
2053
|
+
return (await this.documentIndexer.getDirectedRelationships(sourceId, targetId, [relationshipType], {
|
|
2054
|
+
cursor: "0",
|
|
2055
|
+
limit: 1
|
|
2056
|
+
}, void 0, signal)).results[0];
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* The id a write on `identifier` is stored under, which its signatures bind.
|
|
2060
|
+
* A create names its own id, and an id no slug maps to is taken as given, so
|
|
2061
|
+
* a document still in flight resolves to itself.
|
|
2062
|
+
*/
|
|
2063
|
+
async resolveWriteTarget(identifier, branch, actions, signal) {
|
|
2064
|
+
if (actions.some((action) => action.type === "CREATE_DOCUMENT")) return identifier;
|
|
2065
|
+
const view = { branch };
|
|
2066
|
+
const bySlug = await this.documentView.resolveSlug(identifier, view, void 0, signal);
|
|
2067
|
+
if (bySlug === void 0 || bySlug === identifier) return identifier;
|
|
2068
|
+
return this.documentView.resolveIdOrSlug(identifier, view, void 0, signal);
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* The decision model for one target document, built at its stream heads.
|
|
2072
|
+
*
|
|
2073
|
+
* The document is fetched unfiltered, because the policy is what decides:
|
|
2074
|
+
* reading it through the read gate would withhold the very scopes the
|
|
2075
|
+
* decision is about. A deleted document is served at its deletion boundary,
|
|
2076
|
+
* which is what lets the model refuse an execute against it -- authEnforcement
|
|
2077
|
+
* requires documentDecisions, so that read is available whenever this runs.
|
|
2078
|
+
*
|
|
2079
|
+
* Reading past the gate discloses nothing a submit does not. The `auth` and
|
|
2080
|
+
* `document` scopes are readable by every holder, so a verdict resting on the
|
|
2081
|
+
* policy alone is one the caller could compute unaided; and a verdict resting
|
|
2082
|
+
* on a conditional grant reads the executing scope's state exactly as
|
|
2083
|
+
* admission reads it, so the answer here is what submitting and being refused
|
|
2084
|
+
* would have revealed anyway.
|
|
2085
|
+
*
|
|
2086
|
+
* The append condition the build records is dropped. It guards a write, and
|
|
2087
|
+
* this makes none; reproducing it is also what the preflight cannot do, which
|
|
2088
|
+
* is why the answer is a prediction.
|
|
2089
|
+
*/
|
|
2090
|
+
async buildEvaluationTarget(config, documentId, branch, signal) {
|
|
2091
|
+
const document = await this.documentView.get(documentId, { branch }, void 0, signal);
|
|
2092
|
+
const target = {
|
|
2093
|
+
documentId,
|
|
2094
|
+
branch
|
|
2095
|
+
};
|
|
2096
|
+
const built = await buildDecisionModel(new SeededStateReader(this.documentView, document, branch), config.model, target, signal);
|
|
2097
|
+
return {
|
|
2098
|
+
definition: config.model(target),
|
|
2099
|
+
model: built.model,
|
|
2100
|
+
scopeStates: document.state ?? {}
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
1845
2103
|
async removeAllIncomingRelationships(documentId, signal) {
|
|
1846
2104
|
const incoming = await this.documentIndexer.getIncoming(documentId, void 0, void 0, void 0, signal);
|
|
1847
2105
|
const jobs = [];
|
|
@@ -8827,7 +9085,7 @@ var ReactorBuilder = class {
|
|
|
8827
9085
|
poolInstrumentations.push(forwarder);
|
|
8828
9086
|
this.instrumentedPools.push(forwarder);
|
|
8829
9087
|
}
|
|
8830
|
-
const { ProjectionShardManager } = await import("./projection-shard-manager-
|
|
9088
|
+
const { ProjectionShardManager } = await import("./projection-shard-manager-BpcqDtnQ.js");
|
|
8831
9089
|
const manager = new ProjectionShardManager({
|
|
8832
9090
|
shardCount: config.shardCount,
|
|
8833
9091
|
db,
|
|
@@ -8871,7 +9129,7 @@ var ReactorBuilder = class {
|
|
|
8871
9129
|
*/
|
|
8872
9130
|
async createDefaultWorkerFactory(numWorkers, db) {
|
|
8873
9131
|
const [{ WorkerHandle }, { createThreadTransport }, { workerEntryPath }] = await Promise.all([
|
|
8874
|
-
import("./worker-handle-
|
|
9132
|
+
import("./worker-handle-C2rLUHwv.js"),
|
|
8875
9133
|
import("./transport-CZDHzfvy.js"),
|
|
8876
9134
|
Promise.resolve().then(() => worker_exports)
|
|
8877
9135
|
]);
|