@phosphorco/bb-identity 0.1.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/LICENSE +21 -0
- package/README.md +465 -0
- package/bb.d.ts +138 -0
- package/client.d.ts +175 -0
- package/dist/bb-entry-runtime.js +2574 -0
- package/dist/client-entry-runtime.js +2705 -0
- package/dist/host-runtime.js +932 -0
- package/dist/index-runtime.js +201 -0
- package/dist/model-runtime.js +207 -0
- package/dist/react-entry-runtime.js +3324 -0
- package/dist/server-runtime.js +263 -0
- package/dist/state-controller-runtime.js +950 -0
- package/dist/state-runtime.js +1200 -0
- package/dist/state-service-runtime.js +380 -0
- package/dist/state-transport-runtime.js +349 -0
- package/dist/testing-runtime.js +4084 -0
- package/host.d.ts +277 -0
- package/index.d.ts +3 -0
- package/model.d.ts +231 -0
- package/package.json +110 -0
- package/react.d.ts +129 -0
- package/server.d.ts +80 -0
- package/state.d.ts +170 -0
- package/testing.d.ts +202 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// model-runtime.ts
|
|
3
|
+
var ID_MAX_LENGTH = 256;
|
|
4
|
+
var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/;
|
|
5
|
+
function failure(code, message, retry = "never") {
|
|
6
|
+
return { code, message, retry };
|
|
7
|
+
}
|
|
8
|
+
function ok(value) {
|
|
9
|
+
return { ok: true, value };
|
|
10
|
+
}
|
|
11
|
+
function err(code, message, retry = "never") {
|
|
12
|
+
return { ok: false, error: failure(code, message, retry) };
|
|
13
|
+
}
|
|
14
|
+
function isBoundedString(value, maximum = ID_MAX_LENGTH) {
|
|
15
|
+
return typeof value === "string" && value.length > 0 && value.length <= maximum;
|
|
16
|
+
}
|
|
17
|
+
function idCodec(kind) {
|
|
18
|
+
return {
|
|
19
|
+
decode(input) {
|
|
20
|
+
const valid = kind === "server-session" ? isBoundedString(input, 4096) && !/[\u0000-\u0020\u007f]/.test(input) : isBoundedString(input) && ID_PATTERN.test(input);
|
|
21
|
+
if (!valid) {
|
|
22
|
+
return err("invalid-input", `Invalid ${kind} identifier.`);
|
|
23
|
+
}
|
|
24
|
+
return ok(input);
|
|
25
|
+
},
|
|
26
|
+
encode(value) {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
var identityKeyCodec = {
|
|
32
|
+
decode(input) {
|
|
33
|
+
if (!isBoundedString(input, 512) || /[\u0000-\u001f\u007f]/.test(input)) {
|
|
34
|
+
return err("invalid-input", "Invalid identity key.");
|
|
35
|
+
}
|
|
36
|
+
return ok(input);
|
|
37
|
+
},
|
|
38
|
+
encode(value) {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// server-runtime.ts
|
|
44
|
+
function sameExpectation(actor, stamp, expectedActor, expectedStamp) {
|
|
45
|
+
return actor === expectedActor && stamp === expectedStamp ? ok(undefined) : err("stale-context", "The request actor or session changed.", "after-refresh");
|
|
46
|
+
}
|
|
47
|
+
function isPerson(value) {
|
|
48
|
+
return value.kind === "person" || value.kind === "default-user";
|
|
49
|
+
}
|
|
50
|
+
function freezePerson(input) {
|
|
51
|
+
return input.kind === "person" ? Object.freeze({ kind: "person", key: input.key, issuer: input.issuer, subject: input.subject }) : Object.freeze({ kind: "default-user", key: input.key, instanceId: input.instanceId });
|
|
52
|
+
}
|
|
53
|
+
function freezeActorReference(input) {
|
|
54
|
+
return input.kind === "machine" ? Object.freeze({ kind: "machine", key: input.key, instanceId: input.instanceId, hostId: input.hostId }) : freezePerson(input);
|
|
55
|
+
}
|
|
56
|
+
function freezeActor(input) {
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
identity: freezeActorReference(input.identity),
|
|
59
|
+
presentation: Object.freeze({
|
|
60
|
+
displayName: input.presentation.displayName,
|
|
61
|
+
handle: input.presentation.handle,
|
|
62
|
+
avatarUrl: input.presentation.avatarUrl
|
|
63
|
+
}),
|
|
64
|
+
evidence: input.evidence
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function freezeExpected(input) {
|
|
68
|
+
return Object.freeze({ actor: input.actor, session: input.session });
|
|
69
|
+
}
|
|
70
|
+
function createIdentityServer(options) {
|
|
71
|
+
const { host } = options;
|
|
72
|
+
let disposed = false;
|
|
73
|
+
const dispose = () => {
|
|
74
|
+
disposed = true;
|
|
75
|
+
};
|
|
76
|
+
const commits = {
|
|
77
|
+
boundary: "same-process-synchronous",
|
|
78
|
+
validate(target, scope) {
|
|
79
|
+
if (typeof target !== "object" || target === null || target.intent !== "write" || typeof target.snapshot !== "function" || !(target.signal instanceof AbortSignal)) {
|
|
80
|
+
return err("invalid-input", "Write target does not have write intent.");
|
|
81
|
+
}
|
|
82
|
+
if (disposed || target.signal.aborted)
|
|
83
|
+
return err("expired", "The request context is no longer live.");
|
|
84
|
+
const addressIsConcrete = scope.collection.length > 0 && scope.recordId.length > 0 && Number.isInteger(scope.schemaVersion) && scope.schemaVersion >= 0;
|
|
85
|
+
try {
|
|
86
|
+
const snapshot = target.snapshot();
|
|
87
|
+
const capturedActor = snapshot.actor.identity.key;
|
|
88
|
+
const capturedSubject = snapshot.subject.key;
|
|
89
|
+
const capturedExpected = snapshot.expected;
|
|
90
|
+
const capturedFactsAreConsistent = capturedExpected.actor === capturedActor;
|
|
91
|
+
return addressIsConcrete && scope.instanceId === host.instanceId && scope.pluginId === host.pluginId && capturedFactsAreConsistent && scope.subject === capturedSubject && scope.expected.actor === capturedExpected.actor && scope.expected.session === capturedExpected.session ? ok(undefined) : err("stale-context", "Target does not match this live commit scope.", "after-refresh");
|
|
92
|
+
} catch {
|
|
93
|
+
return err("invalid-input", "Write target snapshot is unavailable.");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
async function issueTarget(live, request, input) {
|
|
98
|
+
if (request.signal.aborted)
|
|
99
|
+
return err("expired", "The request context is no longer live.");
|
|
100
|
+
const currentRequest = live.validate(request.expected);
|
|
101
|
+
if (!currentRequest.ok)
|
|
102
|
+
return currentRequest;
|
|
103
|
+
const actor = freezeActor(request.actor);
|
|
104
|
+
let subject = freezeActorReference(actor.identity);
|
|
105
|
+
if (input.selection.kind === "person") {
|
|
106
|
+
if (input.policy.kind === "self-only")
|
|
107
|
+
return err("unsupported", "This operation only supports the acting person.");
|
|
108
|
+
const profiles = await host.directory.getMany({ keys: [input.selection.key] });
|
|
109
|
+
if (!profiles.ok)
|
|
110
|
+
return profiles;
|
|
111
|
+
const found = profiles.value[0];
|
|
112
|
+
if (!found || found.status !== "found" || !isPerson(found.profile.identity))
|
|
113
|
+
return err("not-found", "Selected person was not found.");
|
|
114
|
+
subject = freezePerson(found.profile.identity);
|
|
115
|
+
}
|
|
116
|
+
if (input.intent === "write") {
|
|
117
|
+
const current = sameExpectation(request.actor.identity.key, request.expected.session, input.expected.actor, input.expected.session);
|
|
118
|
+
if (!current.ok)
|
|
119
|
+
return current;
|
|
120
|
+
if (input.expectedSubject !== subject.key)
|
|
121
|
+
return err("stale-owner", "Selected subject changed.", "after-refresh");
|
|
122
|
+
}
|
|
123
|
+
const expected = freezeExpected({ actor: actor.identity.key, session: request.expected.session });
|
|
124
|
+
const snapshot = Object.freeze({
|
|
125
|
+
actor,
|
|
126
|
+
subject,
|
|
127
|
+
expected
|
|
128
|
+
});
|
|
129
|
+
const target = Object.freeze({
|
|
130
|
+
intent: input.intent,
|
|
131
|
+
snapshot: () => snapshot,
|
|
132
|
+
signal: request.signal
|
|
133
|
+
});
|
|
134
|
+
return ok(target);
|
|
135
|
+
}
|
|
136
|
+
const server = {
|
|
137
|
+
instanceId: host.instanceId,
|
|
138
|
+
directory: host.directory,
|
|
139
|
+
participants: host.participants,
|
|
140
|
+
history: host.history,
|
|
141
|
+
commits,
|
|
142
|
+
session: (context, readOptions) => host.session(context, readOptions),
|
|
143
|
+
selfProfile: (context, readOptions) => host.selfProfile(context, readOptions),
|
|
144
|
+
subscribe: (listener) => host.subscribe(listener),
|
|
145
|
+
async personRequest(context, readOptions) {
|
|
146
|
+
if (disposed)
|
|
147
|
+
return err("disposed", "Identity server is disposed.");
|
|
148
|
+
const opened = await host.openPersonRequest(context, readOptions);
|
|
149
|
+
if (!opened.ok)
|
|
150
|
+
return opened;
|
|
151
|
+
const live = opened.value;
|
|
152
|
+
let released = false;
|
|
153
|
+
let request;
|
|
154
|
+
async function target(input) {
|
|
155
|
+
return issueTarget(live, request, input);
|
|
156
|
+
}
|
|
157
|
+
request = {
|
|
158
|
+
actor: live.session.actor,
|
|
159
|
+
expected: { actor: live.session.actor.identity.key, session: live.session.stamp },
|
|
160
|
+
capabilities: live.session.capabilities,
|
|
161
|
+
signal: live.signal,
|
|
162
|
+
target,
|
|
163
|
+
async send(input, sendOptions) {
|
|
164
|
+
const valid = live.validate(input.expected);
|
|
165
|
+
if (!valid.ok)
|
|
166
|
+
return { status: "rejected", error: valid.error };
|
|
167
|
+
return host.send(live, input, sendOptions);
|
|
168
|
+
},
|
|
169
|
+
async callPlugin(input, callOptions) {
|
|
170
|
+
const valid = live.validate(request.expected);
|
|
171
|
+
if (!valid.ok)
|
|
172
|
+
return valid;
|
|
173
|
+
try {
|
|
174
|
+
return input.output.decode(await host.forward(live, input.pluginId, input.method, input.payload));
|
|
175
|
+
} catch {
|
|
176
|
+
return err("unavailable", "Destination plugin call failed.", callOptions?.signal?.aborted ? "never" : "after-reconnect");
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
dispose() {
|
|
180
|
+
if (released)
|
|
181
|
+
return;
|
|
182
|
+
released = true;
|
|
183
|
+
live.release();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
return ok(request);
|
|
187
|
+
},
|
|
188
|
+
toolProvenance: (context) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.readToolProvenance(context),
|
|
189
|
+
sendExternal: (author, input, readOptions) => disposed ? Promise.resolve({ status: "rejected", error: { code: "disposed", message: "Identity server is disposed.", retry: "never" } }) : host.sendExternal(author, input, readOptions),
|
|
190
|
+
lookupOperation: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.lookupOperation(operationId, readOptions),
|
|
191
|
+
dispose
|
|
192
|
+
};
|
|
193
|
+
return server;
|
|
194
|
+
}
|
|
195
|
+
function createIdentityEndpoint(options) {
|
|
196
|
+
let disposed = false;
|
|
197
|
+
let stop = null;
|
|
198
|
+
const dispose = () => {
|
|
199
|
+
disposed = true;
|
|
200
|
+
const unsubscribe = stop;
|
|
201
|
+
stop = null;
|
|
202
|
+
unsubscribe?.();
|
|
203
|
+
};
|
|
204
|
+
const subscribed = options.server.subscribe((event) => {
|
|
205
|
+
if (disposed)
|
|
206
|
+
return;
|
|
207
|
+
try {
|
|
208
|
+
if (event.kind === "directory") {
|
|
209
|
+
const keys = event.keys.map((key) => identityKeyCodec.decode(key));
|
|
210
|
+
options.publish({ kind: "directory", keys: keys.every((key) => key.ok) ? keys.map((key) => key.value) : [], revision: null });
|
|
211
|
+
} else if (event.kind === "participants") {
|
|
212
|
+
const ids = event.threadIds.map((id) => idCodec("thread").decode(id));
|
|
213
|
+
if (ids.every((id) => id.ok))
|
|
214
|
+
options.publish({ kind: "participants", threadIds: ids.map((id) => id.value) });
|
|
215
|
+
} else if (event.kind === "session")
|
|
216
|
+
options.publish(event.reason === undefined ? { kind: "session" } : { kind: "session", reason: event.reason });
|
|
217
|
+
else if (event.kind === "reconnected" || event.kind === "disconnected")
|
|
218
|
+
options.publish({ kind: event.kind });
|
|
219
|
+
else if (event.kind === "disposed") {
|
|
220
|
+
dispose();
|
|
221
|
+
options.publish({ kind: "disconnected" });
|
|
222
|
+
}
|
|
223
|
+
} catch {}
|
|
224
|
+
});
|
|
225
|
+
if (disposed)
|
|
226
|
+
subscribed();
|
|
227
|
+
else
|
|
228
|
+
stop = subscribed;
|
|
229
|
+
const reject = () => Promise.resolve(err("disposed", "Identity endpoint is disposed."));
|
|
230
|
+
const fence = async (work) => {
|
|
231
|
+
try {
|
|
232
|
+
const result = await work;
|
|
233
|
+
return disposed ? err("disposed", "Identity endpoint is disposed.") : result;
|
|
234
|
+
} catch {
|
|
235
|
+
return err(disposed ? "disposed" : "unavailable", disposed ? "Identity endpoint is disposed." : "Identity endpoint read failed.");
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
return {
|
|
239
|
+
bootstrap(context, readOptions) {
|
|
240
|
+
return disposed ? reject() : fence(options.server.session(context, readOptions).then(ok));
|
|
241
|
+
},
|
|
242
|
+
selfProfile(context, readOptions) {
|
|
243
|
+
return disposed ? reject() : fence(options.server.selfProfile(context, readOptions));
|
|
244
|
+
},
|
|
245
|
+
search(context, input, readOptions) {
|
|
246
|
+
return disposed ? reject() : fence(options.server.directory.search(input, readOptions));
|
|
247
|
+
},
|
|
248
|
+
profiles(context, input, readOptions) {
|
|
249
|
+
return disposed ? reject() : fence(options.server.directory.getMany(input, readOptions));
|
|
250
|
+
},
|
|
251
|
+
participants(context, input, readOptions) {
|
|
252
|
+
return disposed ? reject() : fence(options.server.participants.list(input, readOptions));
|
|
253
|
+
},
|
|
254
|
+
participantPreviews(context, input, readOptions) {
|
|
255
|
+
return disposed ? reject() : fence(options.server.participants.previews(input, readOptions));
|
|
256
|
+
},
|
|
257
|
+
dispose
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
export {
|
|
261
|
+
createIdentityServer,
|
|
262
|
+
createIdentityEndpoint
|
|
263
|
+
};
|