@phosphorco/bb-identity 0.3.0 → 0.4.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/EDITING.md +131 -0
- package/NATIVE-SOURCES.md +222 -0
- package/README.md +18 -0
- package/dist/bb-entry-runtime.js +107 -87
- package/dist/client-entry-runtime.js +128 -36
- package/dist/editing-runtime.js +1845 -0
- package/dist/host-runtime.js +54 -29
- package/dist/index-runtime.js +21 -0
- package/dist/model-runtime.js +21 -0
- package/dist/native-runtime.js +476 -0
- package/dist/native-testing-runtime.js +11 -0
- package/dist/react-entry-runtime.js +253 -51
- package/dist/state-controller-runtime.js +96 -11
- package/dist/state-runtime.js +95 -11
- package/dist/testing-runtime.js +112 -92
- package/editing.d.ts +137 -0
- package/host.d.ts +1 -0
- package/native-testing.d.ts +15 -0
- package/native.d.ts +100 -0
- package/package.json +20 -3
- package/react.d.ts +16 -0
package/dist/testing-runtime.js
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// external-person-output-runtime.ts
|
|
3
|
+
function record(value) {
|
|
4
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function normalizeExternalPersonOutput(value) {
|
|
7
|
+
if (!record(value) || !record(value.identity) || !record(value.presentation))
|
|
8
|
+
return value;
|
|
9
|
+
return {
|
|
10
|
+
identity: { kind: value.identity.kind, key: value.identity.key, issuer: value.identity.issuer, subject: value.identity.subject },
|
|
11
|
+
presentation: { displayName: value.presentation.displayName, handle: value.presentation.handle, avatarUrl: value.presentation.avatarUrl },
|
|
12
|
+
evidence: value.evidence
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function normalizeContributionOutput(value) {
|
|
16
|
+
return record(value) && value.personAttribution !== undefined ? { ...value, personAttribution: normalizeExternalPersonOutput(value.personAttribution) } : value;
|
|
17
|
+
}
|
|
18
|
+
function normalizeProvenanceOutput(value) {
|
|
19
|
+
return record(value) && Array.isArray(value.contributions) ? { ...value, contributions: value.contributions.map(normalizeContributionOutput) } : value;
|
|
20
|
+
}
|
|
21
|
+
function normalizeEvidencePageOutput(value, normalize) {
|
|
22
|
+
return record(value) && Array.isArray(value.items) ? { ...value, items: value.items.map(normalize) } : value;
|
|
23
|
+
}
|
|
24
|
+
|
|
2
25
|
// model-runtime.ts
|
|
3
26
|
var ID_MAX_LENGTH = 256;
|
|
4
27
|
var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/;
|
|
@@ -165,6 +188,7 @@ function hasCompleteCausality(groups, contributions, missing) {
|
|
|
165
188
|
}
|
|
166
189
|
var provenanceCodec = {
|
|
167
190
|
decode(input) {
|
|
191
|
+
input = normalizeProvenanceOutput(input);
|
|
168
192
|
if (!isRecord(input) || input.status !== "known" && input.status !== "partial" && input.status !== "unknown") {
|
|
169
193
|
return err("invalid-input", "Invalid execution provenance.");
|
|
170
194
|
}
|
|
@@ -470,11 +494,11 @@ function createEnhancedDirectory(protocol) {
|
|
|
470
494
|
}
|
|
471
495
|
mappings.set(key, mapping);
|
|
472
496
|
};
|
|
473
|
-
const profile = (source,
|
|
474
|
-
if (!isRecord2(
|
|
497
|
+
const profile = (source, record2, revision) => {
|
|
498
|
+
if (!isRecord2(record2) || !source.issuers.includes(record2.issuer) || !idCodec("issuer").decode(record2.issuer).ok || !idCodec("subject").decode(record2.subject).ok || !isProfilePresentation(record2.presentation) || record2.status !== "current" && record2.status !== "historical" || !idCodec("revision").decode(revision).ok)
|
|
475
499
|
return err("incompatible", "Provider directory record is malformed.");
|
|
476
|
-
const issuer =
|
|
477
|
-
const subject =
|
|
500
|
+
const issuer = record2.issuer;
|
|
501
|
+
const subject = record2.subject;
|
|
478
502
|
if (typeof issuer !== "string" || typeof subject !== "string")
|
|
479
503
|
return err("incompatible", "Provider directory record is malformed.");
|
|
480
504
|
let person;
|
|
@@ -485,10 +509,10 @@ function createEnhancedDirectory(protocol) {
|
|
|
485
509
|
}
|
|
486
510
|
if (!person.ok)
|
|
487
511
|
return person;
|
|
488
|
-
if (person.value.kind !== "person" || person.value.issuer !==
|
|
512
|
+
if (person.value.kind !== "person" || person.value.issuer !== record2.issuer || person.value.subject !== record2.subject || !identityKeyCodec.decode(person.value.key).ok) {
|
|
489
513
|
return err("incompatible", "Provider directory returned an invalid person reference.");
|
|
490
514
|
}
|
|
491
|
-
return profileCodec.decode({ identity: person.value, presentation:
|
|
515
|
+
return profileCodec.decode({ identity: person.value, presentation: record2.presentation, revision, status: record2.status });
|
|
492
516
|
};
|
|
493
517
|
return {
|
|
494
518
|
async search(input, options) {
|
|
@@ -514,8 +538,8 @@ function createEnhancedDirectory(protocol) {
|
|
|
514
538
|
if (!Array.isArray(result.value.records) || result.value.records.length > input.limit || result.value.nextCursor !== null && !idCodec("cursor").decode(result.value.nextCursor).ok || !idCodec("revision").decode(result.value.revision).ok)
|
|
515
539
|
return err("incompatible", "Provider directory page is malformed.");
|
|
516
540
|
const profiles = [];
|
|
517
|
-
for (const
|
|
518
|
-
const decoded = profile(current.value,
|
|
541
|
+
for (const record2 of result.value.records) {
|
|
542
|
+
const decoded = profile(current.value, record2, result.value.revision);
|
|
519
543
|
if (!decoded.ok)
|
|
520
544
|
return decoded;
|
|
521
545
|
if (input.history === "current" && decoded.value.status !== "current")
|
|
@@ -824,7 +848,7 @@ function createHostAdapter(options) {
|
|
|
824
848
|
} catch {
|
|
825
849
|
return err("unavailable", "Contribution history is unavailable.", "after-reconnect");
|
|
826
850
|
}
|
|
827
|
-
return result.ok ? decodeEvidencePage(result.value, isContribution2) : result;
|
|
851
|
+
return result.ok ? decodeEvidencePage(normalizeEvidencePageOutput(result.value, normalizeContributionOutput), isContribution2) : result;
|
|
828
852
|
},
|
|
829
853
|
async attempts(query, readOptions) {
|
|
830
854
|
if (!validAttemptQuery(query))
|
|
@@ -837,7 +861,7 @@ function createHostAdapter(options) {
|
|
|
837
861
|
} catch {
|
|
838
862
|
return err("unavailable", "Execution history is unavailable.", "after-reconnect");
|
|
839
863
|
}
|
|
840
|
-
return result.ok ? decodeEvidencePage(result.value, (item) => provenanceCodec.decode(item).ok) : result;
|
|
864
|
+
return result.ok ? decodeEvidencePage(normalizeEvidencePageOutput(result.value, normalizeProvenanceOutput), (item) => provenanceCodec.decode(item).ok) : result;
|
|
841
865
|
}
|
|
842
866
|
};
|
|
843
867
|
const subscription = protocol.subscribe((event) => {
|
|
@@ -934,16 +958,12 @@ function createHostAdapter(options) {
|
|
|
934
958
|
};
|
|
935
959
|
}
|
|
936
960
|
function decodeExternalContribution(value, strict = true) {
|
|
937
|
-
if (!isRecord2(value) || value.version !== 1 || !isRecord2(value.author) || strict && Object.keys(value).some((key) => !["version", "author", "personAttribution"].includes(key)) || strict && Object.keys(value.author).some((key) => !["subject", "presentation"].includes(key)) || strict && isRecord2(value.author.presentation) && Object.keys(value.author.presentation).some((key) => !["displayName", "handle", "avatarUrl"].includes(key)) ||
|
|
961
|
+
if (!isRecord2(value) || value.version !== 1 || !isRecord2(value.author) || strict && Object.keys(value).some((key) => !["version", "author", "personAttribution"].includes(key)) || strict && Object.keys(value.author).some((key) => !["subject", "presentation"].includes(key)) || strict && isRecord2(value.author.presentation) && Object.keys(value.author.presentation).some((key) => !["displayName", "handle", "avatarUrl"].includes(key)) || (typeof value.author.subject !== "string" || value.author.subject.length === 0 || value.author.subject.length > 256) || !isProfilePresentation(value.author.presentation)) {
|
|
938
962
|
return err("invalid-input", "Invalid V1 external contribution.");
|
|
939
963
|
}
|
|
940
964
|
let person = value.personAttribution;
|
|
941
|
-
if (!strict
|
|
942
|
-
person =
|
|
943
|
-
identity: { kind: person.identity.kind, key: person.identity.key, issuer: person.identity.issuer, subject: person.identity.subject },
|
|
944
|
-
presentation: isRecord2(person.presentation) ? { displayName: person.presentation.displayName, handle: person.presentation.handle, avatarUrl: person.presentation.avatarUrl } : person.presentation,
|
|
945
|
-
evidence: person.evidence
|
|
946
|
-
};
|
|
965
|
+
if (!strict)
|
|
966
|
+
person = normalizeExternalPersonOutput(person);
|
|
947
967
|
if (strict && isRecord2(person) && isRecord2(person.presentation) && Object.keys(person.presentation).some((key) => !["displayName", "handle", "avatarUrl"].includes(key)))
|
|
948
968
|
return err("invalid-input", "Unexpected presentation field.");
|
|
949
969
|
const decoded = person === undefined ? null : externalPersonSnapshotCodec.decode(person);
|
|
@@ -966,6 +986,16 @@ function canonicalExternal(value) {
|
|
|
966
986
|
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalExternal(value[key])}`).join(",")}}`;
|
|
967
987
|
return JSON.stringify(value);
|
|
968
988
|
}
|
|
989
|
+
function decodeExternalContributionOutcomeV1(raw) {
|
|
990
|
+
const decoded = decodeAcceptance(raw);
|
|
991
|
+
if (!decoded.ok || decoded.value.status !== "submitted")
|
|
992
|
+
return decoded;
|
|
993
|
+
if (decoded.value.receipt.evidence !== "host-accepted")
|
|
994
|
+
return err("incompatible", "V1 needs durable native acceptance.");
|
|
995
|
+
const receipt = decoded.value.receipt;
|
|
996
|
+
const contribution = decodeExternalContribution(receipt.externalContribution, false);
|
|
997
|
+
return contribution.ok ? ok({ status: "submitted", receipt: { ...decoded.value.receipt, externalContribution: contribution.value } }) : err("incompatible", "Host receipt omitted or changed V1 attribution.");
|
|
998
|
+
}
|
|
969
999
|
function externalContributionMethods(upstream, extension, disposed) {
|
|
970
1000
|
const support = () => {
|
|
971
1001
|
if (disposed())
|
|
@@ -985,16 +1015,6 @@ function externalContributionMethods(upstream, extension, disposed) {
|
|
|
985
1015
|
return err("incompatible", "Malformed V1 external contribution capability.");
|
|
986
1016
|
return ok(value);
|
|
987
1017
|
};
|
|
988
|
-
const outcome = (raw) => {
|
|
989
|
-
const decoded = decodeAcceptance(raw);
|
|
990
|
-
if (!decoded.ok || decoded.value.status !== "submitted")
|
|
991
|
-
return decoded;
|
|
992
|
-
if (decoded.value.receipt.evidence !== "host-accepted")
|
|
993
|
-
return err("incompatible", "V1 needs durable native acceptance.");
|
|
994
|
-
const receipt = decoded.value.receipt;
|
|
995
|
-
const contribution = decodeExternalContribution(receipt.externalContribution, false);
|
|
996
|
-
return contribution.ok ? ok({ status: "submitted", receipt: { ...decoded.value.receipt, externalContribution: contribution.value } }) : err("incompatible", "Host receipt omitted or changed V1 attribution.");
|
|
997
|
-
};
|
|
998
1018
|
return {
|
|
999
1019
|
externalContributionCapabilities() {
|
|
1000
1020
|
const negotiated = support();
|
|
@@ -1015,7 +1035,7 @@ function externalContributionMethods(upstream, extension, disposed) {
|
|
|
1015
1035
|
const frozen = JSON.parse(JSON.stringify(decoded.value));
|
|
1016
1036
|
const wire = JSON.parse(JSON.stringify(wireInput(input, upstream.inputCodec)));
|
|
1017
1037
|
try {
|
|
1018
|
-
const result =
|
|
1038
|
+
const result = decodeExternalContributionOutcomeV1(await negotiated.value.acceptV1({ contribution: frozen, input: wire }, options));
|
|
1019
1039
|
if (!result.ok)
|
|
1020
1040
|
return { status: "indeterminate", operationId, message: result.error.message };
|
|
1021
1041
|
if (result.value.status === "submitted" && (result.value.receipt.operationId !== operationId || canonicalExternal(result.value.receipt.externalContribution) !== canonicalExternal(frozen))) {
|
|
@@ -1037,7 +1057,7 @@ function externalContributionMethods(upstream, extension, disposed) {
|
|
|
1037
1057
|
if (!result.ok)
|
|
1038
1058
|
return result;
|
|
1039
1059
|
if (isRecord2(result.value) && result.value.status === "final") {
|
|
1040
|
-
const decoded =
|
|
1060
|
+
const decoded = decodeExternalContributionOutcomeV1(result.value.outcome);
|
|
1041
1061
|
if (decoded.ok && decoded.value.status === "submitted" && decoded.value.receipt.operationId !== operationId) {
|
|
1042
1062
|
return err("incompatible", "Host lookup returned a receipt for another operation.");
|
|
1043
1063
|
}
|
|
@@ -1991,39 +2011,39 @@ function createBbUpstreamDriver(options) {
|
|
|
1991
2011
|
const active = new Set;
|
|
1992
2012
|
const listeners = new Set;
|
|
1993
2013
|
const labelledExternal = new WeakSet;
|
|
1994
|
-
const terminateScope = (
|
|
2014
|
+
const terminateScope = (record2, scope) => {
|
|
1995
2015
|
if (!scope || scope.released)
|
|
1996
2016
|
return;
|
|
1997
2017
|
scope.released = true;
|
|
1998
|
-
|
|
2018
|
+
record2.controller.signal.removeEventListener("abort", scope.parentAbort);
|
|
1999
2019
|
scope.controller.abort();
|
|
2000
|
-
|
|
2020
|
+
record2.scopes.delete(scope);
|
|
2001
2021
|
};
|
|
2002
|
-
const terminate = (
|
|
2003
|
-
for (const scope of [...
|
|
2004
|
-
terminateScope(
|
|
2005
|
-
|
|
2006
|
-
active.delete(
|
|
2022
|
+
const terminate = (record2) => {
|
|
2023
|
+
for (const scope of [...record2.scopes])
|
|
2024
|
+
terminateScope(record2, scope);
|
|
2025
|
+
record2.controller.abort();
|
|
2026
|
+
active.delete(record2);
|
|
2007
2027
|
};
|
|
2008
2028
|
const known = (context) => {
|
|
2009
|
-
const
|
|
2010
|
-
return !
|
|
2029
|
+
const record2 = records.get(context);
|
|
2030
|
+
return !record2 ? err("invalid-input", "Invocation context was not issued by this binding.") : disposed ? err("disposed", "Upstream binding is disposed.") : record2.controller.signal.aborted ? err("expired", "Invocation context has expired.") : ok(record2);
|
|
2011
2031
|
};
|
|
2012
2032
|
const admission = (context) => {
|
|
2013
|
-
const
|
|
2014
|
-
if (!
|
|
2015
|
-
return
|
|
2016
|
-
if (
|
|
2033
|
+
const record2 = known(context);
|
|
2034
|
+
if (!record2.ok)
|
|
2035
|
+
return record2;
|
|
2036
|
+
if (record2.value.origin === "external")
|
|
2017
2037
|
return err("unauthenticated", "External upstream work uses an explicit external source.");
|
|
2018
|
-
return
|
|
2038
|
+
return record2;
|
|
2019
2039
|
};
|
|
2020
2040
|
const session = (context) => {
|
|
2021
|
-
const
|
|
2022
|
-
if (!
|
|
2023
|
-
return unavailableSession(instanceId,
|
|
2024
|
-
if (
|
|
2041
|
+
const record2 = known(context);
|
|
2042
|
+
if (!record2.ok)
|
|
2043
|
+
return unavailableSession(instanceId, record2.error.code === "invalid-input" ? "incompatible" : "unauthenticated", record2.error.message);
|
|
2044
|
+
if (record2.value.origin === "interactive-user")
|
|
2025
2045
|
return readySession();
|
|
2026
|
-
return
|
|
2046
|
+
return record2.value.origin === "background" ? machineSession() : unavailableSession(instanceId, "unauthenticated", "External upstream work uses an explicit external source.");
|
|
2027
2047
|
};
|
|
2028
2048
|
const driver = {
|
|
2029
2049
|
instanceId,
|
|
@@ -2041,20 +2061,20 @@ function createBbUpstreamDriver(options) {
|
|
|
2041
2061
|
const allowed = admission(context);
|
|
2042
2062
|
if (!allowed.ok)
|
|
2043
2063
|
return allowed;
|
|
2044
|
-
const
|
|
2064
|
+
const record2 = allowed.value;
|
|
2045
2065
|
let scope;
|
|
2046
|
-
const parentAbort = () => terminateScope(
|
|
2066
|
+
const parentAbort = () => terminateScope(record2, scope);
|
|
2047
2067
|
scope = { controller: new AbortController, parentAbort, released: false };
|
|
2048
|
-
|
|
2049
|
-
const expire = () => terminateScope(
|
|
2050
|
-
|
|
2051
|
-
const exposedSession =
|
|
2052
|
-
const expectedKey =
|
|
2068
|
+
record2.scopes.add(scope);
|
|
2069
|
+
const expire = () => terminateScope(record2, scope);
|
|
2070
|
+
record2.controller.signal.addEventListener("abort", parentAbort, { once: true });
|
|
2071
|
+
const exposedSession = record2.origin === "background" ? machineSession() : readySession();
|
|
2072
|
+
const expectedKey = record2.origin === "background" ? machineKey.value : singletonKey;
|
|
2053
2073
|
return ok({
|
|
2054
2074
|
signal: scope.controller.signal,
|
|
2055
2075
|
session: exposedSession,
|
|
2056
2076
|
validate(expected) {
|
|
2057
|
-
if (scope.released || scope.controller.signal.aborted ||
|
|
2077
|
+
if (scope.released || scope.controller.signal.aborted || record2.controller.signal.aborted || disposed) {
|
|
2058
2078
|
return err("expired", "Upstream request context is no longer live.");
|
|
2059
2079
|
}
|
|
2060
2080
|
return expected.actor === expectedKey && expected.session === stamp.value ? ok(undefined) : err("stale-context", "Request expectation does not match the singleton session.");
|
|
@@ -2138,22 +2158,22 @@ function createBbUpstreamDriver(options) {
|
|
|
2138
2158
|
throw new Error("Upstream binding is disposed.");
|
|
2139
2159
|
const controller = new AbortController;
|
|
2140
2160
|
const invocation = Object.freeze({ origin, signal: controller.signal });
|
|
2141
|
-
const
|
|
2142
|
-
records.set(invocation,
|
|
2143
|
-
active.add(
|
|
2161
|
+
const record2 = { origin, controller, scopes: new Set };
|
|
2162
|
+
records.set(invocation, record2);
|
|
2163
|
+
active.add(record2);
|
|
2144
2164
|
return invocation;
|
|
2145
2165
|
},
|
|
2146
2166
|
release(invocation) {
|
|
2147
|
-
const
|
|
2148
|
-
if (
|
|
2149
|
-
terminate(
|
|
2167
|
+
const record2 = records.get(invocation);
|
|
2168
|
+
if (record2)
|
|
2169
|
+
terminate(record2);
|
|
2150
2170
|
},
|
|
2151
2171
|
dispose() {
|
|
2152
2172
|
if (disposed)
|
|
2153
2173
|
return;
|
|
2154
2174
|
disposed = true;
|
|
2155
|
-
for (const
|
|
2156
|
-
terminate(
|
|
2175
|
+
for (const record2 of [...active])
|
|
2176
|
+
terminate(record2);
|
|
2157
2177
|
for (const listener of [...listeners]) {
|
|
2158
2178
|
try {
|
|
2159
2179
|
listener({ kind: "disposed" });
|
|
@@ -2780,8 +2800,8 @@ function createBbIdentityServerBinding(options) {
|
|
|
2780
2800
|
const invocations = new WeakMap;
|
|
2781
2801
|
const expired = () => err("expired", "Invocation has already settled.");
|
|
2782
2802
|
const recordFor = (invocation) => {
|
|
2783
|
-
const
|
|
2784
|
-
return disposed || !
|
|
2803
|
+
const record2 = invocations.get(invocation);
|
|
2804
|
+
return disposed || !record2 || record2.settled || record2.signal.aborted ? expired() : ok(record2);
|
|
2785
2805
|
};
|
|
2786
2806
|
const unavailableSession2 = () => ({
|
|
2787
2807
|
status: "unavailable",
|
|
@@ -2789,25 +2809,25 @@ function createBbIdentityServerBinding(options) {
|
|
|
2789
2809
|
error: { code: "expired", message: "Invocation has already settled.", retry: "after-refresh" }
|
|
2790
2810
|
});
|
|
2791
2811
|
const openPerson = async (invocation) => {
|
|
2792
|
-
const
|
|
2793
|
-
if (!
|
|
2794
|
-
return
|
|
2795
|
-
const opened = await server.personRequest(
|
|
2796
|
-
if (
|
|
2812
|
+
const record2 = recordFor(invocation);
|
|
2813
|
+
if (!record2.ok)
|
|
2814
|
+
return record2;
|
|
2815
|
+
const opened = await server.personRequest(record2.value.request);
|
|
2816
|
+
if (record2.value.settled || record2.value.signal.aborted) {
|
|
2797
2817
|
if (opened.ok)
|
|
2798
2818
|
opened.value.dispose();
|
|
2799
2819
|
return expired();
|
|
2800
2820
|
}
|
|
2801
2821
|
if (opened.ok)
|
|
2802
|
-
|
|
2822
|
+
record2.value.people.add(opened.value);
|
|
2803
2823
|
return opened;
|
|
2804
2824
|
};
|
|
2805
2825
|
const guardEndpoint = async (invocation, operation) => {
|
|
2806
|
-
const
|
|
2807
|
-
if (!
|
|
2808
|
-
return
|
|
2809
|
-
const result = await operation(
|
|
2810
|
-
return
|
|
2826
|
+
const record2 = recordFor(invocation);
|
|
2827
|
+
if (!record2.ok)
|
|
2828
|
+
return record2;
|
|
2829
|
+
const result = await operation(record2.value.request);
|
|
2830
|
+
return record2.value.settled || record2.value.signal.aborted ? expired() : result;
|
|
2811
2831
|
};
|
|
2812
2832
|
let disposeBinding = () => {};
|
|
2813
2833
|
const guardedHistory = {
|
|
@@ -2828,11 +2848,11 @@ function createBbIdentityServerBinding(options) {
|
|
|
2828
2848
|
instanceId: server.instanceId,
|
|
2829
2849
|
commits: server.commits,
|
|
2830
2850
|
async session(invocation) {
|
|
2831
|
-
const
|
|
2832
|
-
if (!
|
|
2851
|
+
const record2 = recordFor(invocation);
|
|
2852
|
+
if (!record2.ok)
|
|
2833
2853
|
return unavailableSession2();
|
|
2834
|
-
const session = await server.session(
|
|
2835
|
-
return
|
|
2854
|
+
const session = await server.session(record2.value.request);
|
|
2855
|
+
return record2.value.settled || record2.value.signal.aborted ? unavailableSession2() : session;
|
|
2836
2856
|
},
|
|
2837
2857
|
selfProfile: (invocation) => guardEndpoint(invocation, (request) => server.selfProfile(request)),
|
|
2838
2858
|
personRequest: openPerson,
|
|
@@ -2909,18 +2929,18 @@ function createBbIdentityServerBinding(options) {
|
|
|
2909
2929
|
return endpointRegistered;
|
|
2910
2930
|
}
|
|
2911
2931
|
const createInvocation = (context) => {
|
|
2912
|
-
const
|
|
2932
|
+
const record2 = { request: context.request, signal: context.signal, people: new Set, settled: false };
|
|
2913
2933
|
const invocation = {
|
|
2914
2934
|
origin: context.origin,
|
|
2915
2935
|
signal: context.signal,
|
|
2916
2936
|
person: () => context.origin === "external" ? Promise.resolve(err("unauthenticated", "External invocation uses an explicit external source.")) : openPerson(invocation)
|
|
2917
2937
|
};
|
|
2918
|
-
invocations.set(invocation,
|
|
2938
|
+
invocations.set(invocation, record2);
|
|
2919
2939
|
const finish = () => {
|
|
2920
|
-
|
|
2921
|
-
for (const request of
|
|
2940
|
+
record2.settled = true;
|
|
2941
|
+
for (const request of record2.people)
|
|
2922
2942
|
request.dispose();
|
|
2923
|
-
|
|
2943
|
+
record2.people.clear();
|
|
2924
2944
|
};
|
|
2925
2945
|
return { invocation, finish };
|
|
2926
2946
|
};
|
|
@@ -3136,7 +3156,7 @@ function createMultiUserProtocol(instanceId, initial) {
|
|
|
3136
3156
|
}
|
|
3137
3157
|
};
|
|
3138
3158
|
};
|
|
3139
|
-
const
|
|
3159
|
+
const record2 = (subject) => {
|
|
3140
3160
|
const person = people.get(subject);
|
|
3141
3161
|
return person ? { issuer: ISSUER, subject, presentation: { displayName: person.name, handle: null, avatarUrl: null }, status: "current" } : null;
|
|
3142
3162
|
};
|
|
@@ -3146,11 +3166,11 @@ function createMultiUserProtocol(instanceId, initial) {
|
|
|
3146
3166
|
generation: brand("provider-generation", "wire-fake-provider"),
|
|
3147
3167
|
async directory(input) {
|
|
3148
3168
|
const query = input.query.toLowerCase();
|
|
3149
|
-
const records = [...people.keys()].map(
|
|
3169
|
+
const records = [...people.keys()].map(record2).filter((r) => r !== null && (r.subject.includes(query) || r.presentation.displayName.toLowerCase().includes(query))).slice(0, input.limit);
|
|
3150
3170
|
return ok({ records, nextCursor: null, revision: revision() });
|
|
3151
3171
|
},
|
|
3152
3172
|
async lookup(subjects) {
|
|
3153
|
-
return ok({ revision: revision(), records: subjects.map((s) => ({ issuer: s.issuer, subject: s.subject, record: s.issuer === ISSUER ?
|
|
3173
|
+
return ok({ revision: revision(), records: subjects.map((s) => ({ issuer: s.issuer, subject: s.subject, record: s.issuer === ISSUER ? record2(s.subject) : null })) });
|
|
3154
3174
|
},
|
|
3155
3175
|
person(issuer, subject) {
|
|
3156
3176
|
const key = identityKeyCodec.decode(`${issuer}:${subject}`);
|
|
@@ -3402,8 +3422,8 @@ function canonical(value) {
|
|
|
3402
3422
|
return JSON.stringify(value) ?? "undefined";
|
|
3403
3423
|
if (Array.isArray(value))
|
|
3404
3424
|
return "[" + value.map(canonical).join(",") + "]";
|
|
3405
|
-
const
|
|
3406
|
-
return "{" + Object.keys(
|
|
3425
|
+
const record2 = value;
|
|
3426
|
+
return "{" + Object.keys(record2).filter((k) => record2[k] !== undefined).sort().map((k) => JSON.stringify(k) + ":" + canonical(record2[k])).join(",") + "}";
|
|
3407
3427
|
}
|
|
3408
3428
|
function id(kind, value) {
|
|
3409
3429
|
const decoded = idCodec(kind).decode(value);
|
package/editing.d.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** Opt-in editing/v1. Publication target and actual draft owner remain distinct. */
|
|
2
|
+
import type { Codec, ConflictToken, Disposable, IdentityError, OperationId, OperationLookup, OwnerSessionId, ReadOptions, RequestExpectation, Result, Scheduler, Unsubscribe } from './model.js';
|
|
3
|
+
import type { ServerSession } from './host.js';
|
|
4
|
+
import type { ConflictContext, ConflictDecision, PendingDraft, StateAddress, StateDefinition, StateInvalidation, StateMutation, StateOutcome, StateRead } from './state.js';
|
|
5
|
+
export declare const EDITING_PROTOCOL: 'bb-identity.editing/v1';
|
|
6
|
+
export interface EditingScope<Target> {
|
|
7
|
+
/** Actual actor's draft retention address. Never the owner of a shared document. */
|
|
8
|
+
readonly draftAddress: StateAddress;
|
|
9
|
+
readonly schemaVersion: number;
|
|
10
|
+
/** Feature-owned publication target, distinct from draft actor and session. */
|
|
11
|
+
readonly target: Target;
|
|
12
|
+
readonly expected: RequestExpectation;
|
|
13
|
+
}
|
|
14
|
+
export interface EditingRequest<T, Target> {
|
|
15
|
+
readonly protocol: typeof EDITING_PROTOCOL;
|
|
16
|
+
readonly target: Target;
|
|
17
|
+
readonly mutation: StateMutation<T>;
|
|
18
|
+
}
|
|
19
|
+
export interface EditingDiagnostic { readonly code: string; readonly message: string; readonly path?: readonly (string | number)[]; }
|
|
20
|
+
export type EditingReceipt<T, Target> = {
|
|
21
|
+
readonly protocol: typeof EDITING_PROTOCOL;
|
|
22
|
+
readonly request: EditingRequest<T, Target>;
|
|
23
|
+
readonly outcome: StateOutcome<T> | { readonly status: 'rejected'; readonly operationId: OperationId; readonly diagnostics: readonly EditingDiagnostic[] };
|
|
24
|
+
};
|
|
25
|
+
export type EditingSave<T, Target> = EditingReceipt<T, Target> | {
|
|
26
|
+
readonly protocol: typeof EDITING_PROTOCOL; readonly status: 'indeterminate'; readonly operationId: OperationId;
|
|
27
|
+
};
|
|
28
|
+
export interface EditingTransport<T, Target> {
|
|
29
|
+
/** Construct only after negotiating this version with the feature endpoint; no legacy fallback. */
|
|
30
|
+
readonly protocol: typeof EDITING_PROTOCOL;
|
|
31
|
+
load(scope: EditingScope<Target>, options?: ReadOptions): Promise<Result<StateRead<T>>>;
|
|
32
|
+
save(request: EditingRequest<T, Target>, options?: ReadOptions): Promise<Result<EditingSave<T, Target>>>;
|
|
33
|
+
reconcile(request: EditingRequest<T, Target>, options?: ReadOptions): Promise<Result<OperationLookup<EditingReceipt<T, Target>>>>;
|
|
34
|
+
subscribe(scope: EditingScope<Target>, listener: (event: StateInvalidation) => void): Unsubscribe;
|
|
35
|
+
}
|
|
36
|
+
export interface EditingDraft<T, Target> {
|
|
37
|
+
readonly protocol: typeof EDITING_PROTOCOL;
|
|
38
|
+
readonly target: Target;
|
|
39
|
+
/** desired B and exact inFlight A are independent fields. */
|
|
40
|
+
readonly draft: PendingDraft<T>;
|
|
41
|
+
readonly rejection: EditingReceipt<T, Target> | null;
|
|
42
|
+
}
|
|
43
|
+
export interface EditingCheckpoint<T, Target> { readonly revision: number; readonly draft: EditingDraft<T, Target>; }
|
|
44
|
+
export interface EditingDraftStorage<T, Target> {
|
|
45
|
+
find(scope: EditingScope<Target>, limit: number): Promise<Result<readonly EditingCheckpoint<T, Target>[]>>;
|
|
46
|
+
write(draft: EditingDraft<T, Target>, expectedRevision: number | null): Promise<Result<EditingCheckpoint<T, Target>>>;
|
|
47
|
+
remove(key: { readonly target: Target; readonly draftKey: import('./state.js').DraftKey }, expectedRevision: number): Promise<Result<void>>;
|
|
48
|
+
}
|
|
49
|
+
export interface EditingSnapshot<T, Target> {
|
|
50
|
+
readonly protocol: typeof EDITING_PROTOCOL;
|
|
51
|
+
readonly status: 'loading' | 'loaded' | 'fenced' | 'detached';
|
|
52
|
+
readonly scope: EditingScope<Target>;
|
|
53
|
+
readonly ownerSession: OwnerSessionId;
|
|
54
|
+
readonly desired: T | undefined;
|
|
55
|
+
readonly localGeneration: number;
|
|
56
|
+
readonly acknowledged: StateRead<T> | null;
|
|
57
|
+
readonly dirty: boolean;
|
|
58
|
+
readonly editable: boolean;
|
|
59
|
+
readonly submitted: EditingRequest<T, Target> | null;
|
|
60
|
+
readonly publication: {
|
|
61
|
+
readonly eligible: boolean;
|
|
62
|
+
readonly status: 'idle' | 'saving' | 'unknown' | 'conflict' | 'rejected' | 'fenced';
|
|
63
|
+
readonly message: string | null;
|
|
64
|
+
readonly rejection: EditingReceipt<T, Target> | null;
|
|
65
|
+
readonly conflict: ConflictContext<T> | null;
|
|
66
|
+
};
|
|
67
|
+
readonly checkpoint: {
|
|
68
|
+
readonly status: 'disabled' | 'local-only' | 'writing' | 'backed-up' | 'failed';
|
|
69
|
+
readonly generation: number | null;
|
|
70
|
+
readonly revision: number | null;
|
|
71
|
+
readonly error: IdentityError | null;
|
|
72
|
+
};
|
|
73
|
+
/** Exportable even if persistence failed. Only checkpoint success proves backup. */
|
|
74
|
+
readonly draft: EditingDraft<T, Target> | null;
|
|
75
|
+
}
|
|
76
|
+
export interface EditingState<T, Target> extends Disposable {
|
|
77
|
+
/** Borrowed identity for shared retirement coordination; never disposed by the binding. */
|
|
78
|
+
readonly draftStorage: EditingDraftStorage<T, Target> | null;
|
|
79
|
+
getSnapshot(): EditingSnapshot<T, Target>;
|
|
80
|
+
subscribe(listener: () => void): Unsubscribe;
|
|
81
|
+
start(): Promise<Result<void>>;
|
|
82
|
+
edit(value: T): Result<void>;
|
|
83
|
+
/** Explicit action only; reconcile A never implicitly dispatches B. */
|
|
84
|
+
flush(): Promise<Result<void>>;
|
|
85
|
+
reconnect(options?: { readonly reuseInFlight?: boolean }): Promise<Result<void>>;
|
|
86
|
+
resolveConflict(token: ConflictToken, decision: ConflictDecision<T>): Promise<Result<void>>;
|
|
87
|
+
checkpoint(): Promise<Result<EditingCheckpoint<T, Target> | null>>;
|
|
88
|
+
recover(candidate: EditingCheckpoint<T, Target>): Promise<Result<void>>;
|
|
89
|
+
recoveryCandidates(options?: ReadOptions): Promise<Result<readonly EditingCheckpoint<T, Target>[]>>;
|
|
90
|
+
discardRecovery(candidate: EditingCheckpoint<T, Target>): Promise<Result<void>>;
|
|
91
|
+
detach(reason: 'identity-invalidated' | 'view-change' | 'unmount'): EditingDraft<T, Target> | null;
|
|
92
|
+
close(options: { readonly pending: 'flush' | 'preserve' | 'discard' }): Promise<Result<void>>;
|
|
93
|
+
}
|
|
94
|
+
export declare function createIdentityEditingState<T, Target>(options: {
|
|
95
|
+
readonly draftAddress: StateAddress; readonly target: Target; readonly targetCodec: Codec<Target>;
|
|
96
|
+
readonly expected: RequestExpectation; readonly ownerSession: OwnerSessionId;
|
|
97
|
+
readonly definition: StateDefinition<T>; readonly transport: EditingTransport<T, Target>;
|
|
98
|
+
readonly drafts?: EditingDraftStorage<T, Target>; readonly scheduler?: Scheduler;
|
|
99
|
+
readonly onConflict: (context: ConflictContext<T>) => ConflictDecision<T>;
|
|
100
|
+
readonly initializeEmpty: boolean;
|
|
101
|
+
readonly currentSession: () => Result<ServerSession>;
|
|
102
|
+
}): EditingState<T, Target>;
|
|
103
|
+
export declare function editingCodecs<T, Target>(value: Codec<T>, target: Codec<Target>): {
|
|
104
|
+
/** Actual load/subscription scope. Does not create a mutation or infer shared ownership. */
|
|
105
|
+
readonly scope: Codec<EditingScope<Target>>;
|
|
106
|
+
readonly request: Codec<EditingRequest<T, Target>>;
|
|
107
|
+
readonly receipt: Codec<EditingReceipt<T, Target>>;
|
|
108
|
+
readonly save: Codec<EditingSave<T, Target>>;
|
|
109
|
+
readonly lookup: Codec<OperationLookup<EditingReceipt<T, Target>>>;
|
|
110
|
+
readonly draft: Codec<EditingDraft<T, Target>>;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** Owned by the shared binding lifetime; subject is always the actual actor. */
|
|
114
|
+
export interface EditingBindingOptions<T, Target> {
|
|
115
|
+
readonly resourceKey: string;
|
|
116
|
+
readonly target: Target; readonly targetCodec: Codec<Target>;
|
|
117
|
+
readonly client: import('./client.js').IdentityClient;
|
|
118
|
+
readonly view: import('./client.js').IdentityView;
|
|
119
|
+
create(input: { readonly session: Extract<ServerSession, { status: 'ready' }>;
|
|
120
|
+
readonly subject: import('./model.js').ActorReference; readonly ownerSession: OwnerSessionId }): EditingState<T, Target>;
|
|
121
|
+
readonly onUnpersistedDraft?: (draft: EditingDraft<T, Target>, error: IdentityError) => void;
|
|
122
|
+
}
|
|
123
|
+
export interface IdentityEditingBinding<T, Target> extends Disposable {
|
|
124
|
+
getSnapshot(): EditingSnapshot<T, Target> | { readonly status: 'waiting-for-identity' };
|
|
125
|
+
subscribe(listener: () => void): Unsubscribe;
|
|
126
|
+
currentOwnerSession(): OwnerSessionId | null;
|
|
127
|
+
edit(value: T): Result<void>;
|
|
128
|
+
flush(): Promise<Result<void>>;
|
|
129
|
+
reconnect(owner: OwnerSessionId): Promise<Result<void>>;
|
|
130
|
+
checkpoint(owner: OwnerSessionId): Promise<Result<EditingCheckpoint<T, Target> | null>>;
|
|
131
|
+
recoveryCandidates(owner: OwnerSessionId, options?: ReadOptions): Promise<Result<readonly EditingCheckpoint<T, Target>[]>>;
|
|
132
|
+
recover(owner: OwnerSessionId, checkpoint: EditingCheckpoint<T, Target>): Promise<Result<void>>;
|
|
133
|
+
discardRecovery(owner: OwnerSessionId, checkpoint: EditingCheckpoint<T, Target>): Promise<Result<void>>;
|
|
134
|
+
resolveConflict(owner: OwnerSessionId, token: ConflictToken, decision: ConflictDecision<T>): Promise<Result<void>>;
|
|
135
|
+
}
|
|
136
|
+
export declare function bindIdentityEditingState<T, Target>(options: EditingBindingOptions<T, Target>): IdentityEditingBinding<T, Target>;
|
|
137
|
+
export declare const EDITING_LIMITS: { readonly bytes: number; readonly diagnostics: number; readonly message: number; readonly code: number; readonly path: number };
|
package/host.d.ts
CHANGED
|
@@ -49,6 +49,7 @@ export type ExternalContributionOutcomeV1 = Exclude<AcceptanceOutcome, { status:
|
|
|
49
49
|
readonly externalContribution: ExternalContributionV1;
|
|
50
50
|
};
|
|
51
51
|
};
|
|
52
|
+
export declare const externalContributionOutcomeV1Codec: Codec<ExternalContributionOutcomeV1>;
|
|
52
53
|
/** Context-free support; native storage written by V1 requires a capable host on rollback. */
|
|
53
54
|
export interface ExternalContributionCapabilityV1 {
|
|
54
55
|
readonly version: 1; readonly personKinds: readonly ['person'];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { NativeSourceEvidence, NativeSourceSnapshot } from './native.js';
|
|
2
|
+
import type { Result } from './model.js';
|
|
3
|
+
/** Source conformance only: caller must obtain producer/live receipts separately. */
|
|
4
|
+
export interface NativeSourceConformanceCase {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly snapshot: NativeSourceSnapshot;
|
|
7
|
+
/** Independent semantic oracle, not an expected value generated by the adapter. */
|
|
8
|
+
readonly verify: (result: Result<NativeSourceEvidence>) => void | Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export declare function defineNativeSourceConformance(options: {
|
|
11
|
+
readonly test: (name: string, run: () => void | Promise<void>) => unknown;
|
|
12
|
+
/** Drive the adopter's real source path. The kit does not construct an alternate adapter. */
|
|
13
|
+
readonly resolve: (snapshot: NativeSourceSnapshot) => Result<NativeSourceEvidence> | Promise<Result<NativeSourceEvidence>>;
|
|
14
|
+
readonly cases: readonly NativeSourceConformanceCase[];
|
|
15
|
+
}): void;
|