@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/host.d.ts ADDED
@@ -0,0 +1,277 @@
1
+ /** Integration ports. Raw fork protocol is structural and independent of package brands. */
2
+ import type { AcceptanceOutcome, ActorReference, ActorSnapshot, Codec, Contribution, ContributionReference,
3
+ Directory, Disposable, ExecutionProvenance, IdentityError, IdentityProfile, InstanceId, OperationLookup,
4
+ ParticipantReader, PersonReference, PluginId, ProfilePresentation, ProviderGeneration, IssuerId, ReadOptions, RequestExpectation,
5
+ Result, Scheduler, SendInput, SessionStamp, Unsubscribe, Wire } from './model.js';
6
+
7
+ declare const requestLease: unique symbol;
8
+ export interface IdentityCapabilities {
9
+ readonly requestIdentity: 'singleton' | 'host-resolved';
10
+ readonly acceptance: 'pre-dispatch-check' | 'transactional-check';
11
+ readonly forwarding: 'singleton-convention' | 'host-bound';
12
+ readonly directory: { readonly search: boolean; readonly lookup: boolean };
13
+ readonly participants: boolean;
14
+ readonly externalSend: 'source-labelled' | 'structured';
15
+ readonly toolProvenance: 'unknown' | 'partial' | 'causal';
16
+ readonly operationLookup: boolean;
17
+ }
18
+ /** Wire DTO; loading/disposed are local client states, not server session responses. */
19
+ export type ServerSession =
20
+ | { readonly status: 'ready'; readonly instanceId: InstanceId; readonly mode: 'single-user' | 'multi-user';
21
+ readonly actor: ActorSnapshot & { readonly identity: ActorReference };
22
+ readonly stamp: SessionStamp; readonly capabilities: IdentityCapabilities }
23
+ | { readonly status: 'unauthenticated' | 'unavailable' | 'incompatible'; readonly instanceId: InstanceId; readonly error: IdentityError };
24
+ export declare const serverSessionCodec: Codec<ServerSession>;
25
+ /** Same-process, synchronous validation only; callers must not span an await or process boundary. */
26
+ export interface LiveRequest {
27
+ readonly [requestLease]: true;
28
+ readonly signal: AbortSignal;
29
+ readonly session: Extract<ServerSession, { status: 'ready' }>;
30
+ validate(expected: RequestExpectation): Result<void>;
31
+ /** Releases this adapter-owned scope only; never the shared host service. */
32
+ release(): void;
33
+ }
34
+ export type HostInvalidation =
35
+ | { readonly kind: 'session'; readonly reason: 'actor' | 'provider' | 'capabilities' }
36
+ | { readonly kind: 'directory'; readonly keys: readonly string[] }
37
+ | { readonly kind: 'participants'; readonly threadIds: readonly string[] }
38
+ | { readonly kind: 'disconnected' | 'reconnected' | 'disposed' };
39
+ export interface ExternalAuthorInput { readonly subject: string; readonly presentation: ProfilePresentation; }
40
+ export interface IdentityHost<RequestContext, ToolContext, Input> extends Disposable {
41
+ readonly pluginId: PluginId;
42
+ readonly instanceId: InstanceId;
43
+ session(context: RequestContext, options?: ReadOptions): Promise<ServerSession>;
44
+ /** Resolves the request's current actor from its live session; directory capability is not required. */
45
+ selfProfile(context: RequestContext, options?: ReadOptions): Promise<Result<IdentityProfile>>;
46
+ openPersonRequest(context: RequestContext, options?: ReadOptions): Promise<Result<LiveRequest>>;
47
+ readToolProvenance(context: ToolContext): Promise<Result<ExecutionProvenance>>;
48
+ readonly directory: Directory;
49
+ readonly participants: ParticipantReader;
50
+ readonly history: import('./model.js').ProvenanceReader;
51
+ /** Checks synchronously before upstream dispatch; enhanced host checks again inside acceptance. */
52
+ send(request: LiveRequest, input: SendInput<Input>, options?: ReadOptions): Promise<AcceptanceOutcome>;
53
+ sendExternal(author: ExternalAuthorInput, input: SendInput<Input>, options?: ReadOptions): Promise<AcceptanceOutcome>;
54
+ lookupOperation(operationId: SendInput<Input>['operationId'], options?: ReadOptions): Promise<Result<OperationLookup<AcceptanceOutcome>>>;
55
+ forward(request: LiveRequest, destinationPlugin: string, method: string, input: import('./model.js').Json): Promise<unknown>;
56
+ registerProvider(provider: IdentityProvider): Promise<Result<ProviderRegistrationV1>>;
57
+ subscribe(listener: (event: HostInvalidation) => void): Unsubscribe;
58
+ }
59
+ /** Upstream driver is implemented by /bb, not by each feature. Host namespaces use reserved local storage scope. */
60
+ export interface UpstreamDriver<RequestContext, ToolContext, Input> {
61
+ readonly instanceId: InstanceId;
62
+ readonly pluginId: PluginId;
63
+ readonly inputCodec: Codec<Input>;
64
+ readonly scheduler: Scheduler;
65
+ session(context: RequestContext): Promise<ServerSession>;
66
+ selfProfile(context: RequestContext, options?: ReadOptions): Promise<Result<IdentityProfile>>;
67
+ openScope(context: RequestContext): Promise<Result<HostRequestHandle>>;
68
+ /** Native response only; driver cannot promise request lifecycle checks inside upstream's transaction. */
69
+ submit(input: SendInput<Input>, options?: ReadOptions): Promise<AcceptanceOutcome>;
70
+ labelExternal(author: ExternalAuthorInput, input: readonly Input[]): readonly Input[];
71
+ toolCorrelation(context: ToolContext): ExecutionProvenance;
72
+ /** Ordinary SDK RPC; validates locally before dispatch, without original-request propagation. */
73
+ forward(context: RequestContext, destinationPlugin: string, method: string,
74
+ input: import('./model.js').Json, options?: ReadOptions): Promise<unknown>;
75
+ subscribe(listener: (event: HostInvalidation) => void): Unsubscribe;
76
+ }
77
+
78
+ /** Host-owned, structural in-process object. Not JSON, not a package-private symbol or public credential. */
79
+ export interface HostRequestHandle {
80
+ readonly signal: AbortSignal;
81
+ readonly session: Wire<Extract<ServerSession, { status: 'ready' }>>;
82
+ validate(expected: Wire<RequestExpectation>): Wire<Result<void>>;
83
+ release(): void;
84
+ }
85
+ /** Core dispatch classes; route selection does not select a person actor. */
86
+ export type ForkInvocationRouteClass =
87
+ | 'interactive-session' | 'external-credential' | 'anonymous'
88
+ | 'plugin-background' | 'agent-tool' | 'local-cli';
89
+ /** Fresh per invocation. It is not a provider/plugin generation registration or auth lease. */
90
+ export interface ForkInvocationScope {
91
+ readonly signal: AbortSignal;
92
+ validate(): { readonly ok: true } | { readonly ok: false; readonly code: 'retired' | 'invalidated' };
93
+ release(): void;
94
+ }
95
+ /** Enhanced open-request result. Core returns the request-scoped lifecycle signal for this request/session. */
96
+ export interface ForkRequestHandle extends HostRequestHandle {
97
+ readonly scope: ForkInvocationScope;
98
+ }
99
+ /** Opaque dispatcher context; only the registered returned handler receives it. */
100
+ export interface ForkInvocationContext<RequestContext> {
101
+ readonly request: RequestContext; readonly scope: ForkInvocationScope;
102
+ }
103
+ /** Generation-bound dispatch binding. Disposal is exact-generation conditional. */
104
+ export interface ForkInvocationRegistration extends Disposable {
105
+ readonly generation: string; readonly status: 'staged' | 'active' | 'retired';
106
+ }
107
+ /** One acceptance entry point prevents external work from masquerading as a person scope. */
108
+ export type ForkAcceptanceRequest<Input> = {
109
+ readonly source:
110
+ | { readonly kind: 'scope'; readonly scope: ForkInvocationScope }
111
+ | { readonly kind: 'external'; readonly author: ExternalAuthorInput };
112
+ readonly input: Wire<SendInput<Input>>;
113
+ };
114
+ /** Core-owned in-process registration result. It deliberately has no package brands.
115
+ * The adapter normalizes this to ProviderRegistrationV1 before exposing it to provider authors.
116
+ */
117
+ export interface ForkProviderRegistration extends Disposable {
118
+ readonly generation: string;
119
+ readonly configuration: Readonly<ProviderBoundaryConfigurationV1>;
120
+ getStatus(): 'staged' | 'active' | 'retired';
121
+ readonly signal: AbortSignal;
122
+ subscribe(listener: (status: 'staged' | 'active' | 'retired') => void): Unsubscribe;
123
+ invalidate(change: { readonly kind: 'authentication'; readonly subjects?: readonly { readonly issuer: string; readonly subject: string }[] }
124
+ | { readonly kind: 'directory'; readonly revision: string }): Result<void>;
125
+ /** Successful values use plain strings at the core boundary; adapter brands only after validation. */
126
+ person(issuer: string, subject: string): Wire<Result<PersonReference>>;
127
+ }
128
+ /** Core-facing provider input. It intentionally has no package-private generation brand. */
129
+ export interface ForkIdentityProvider {
130
+ readonly issuers: readonly string[];
131
+ readonly validateReadiness?: (input: { readonly generation: string;
132
+ readonly configuration: Readonly<ProviderBoundaryConfigurationV1>;
133
+ readonly deadlineAt: number; readonly signal: AbortSignal }) => Promise<Wire<Result<void>>>;
134
+ resolve(evidence: ProviderEvidenceV1): Promise<Wire<
135
+ | { readonly status: 'resolved'; readonly issuer: string; readonly subject: string; readonly presentation: ProfilePresentation }
136
+ | { readonly status: 'not-applicable' }
137
+ | { readonly status: 'rejected' | 'unavailable'; readonly reason: string }
138
+ >>;
139
+ directory?(input: { readonly query: string; readonly cursor?: string; readonly limit: number;
140
+ readonly history: 'current' | 'include-historical' }, options?: ReadOptions): Promise<Wire<Result<{
141
+ readonly records: readonly ProviderDirectoryRecord[]; readonly nextCursor: string | null; readonly revision: string;
142
+ }>>>;
143
+ lookup?(subjects: readonly { readonly issuer: string; readonly subject: string }[], options?: ReadOptions): Promise<Wire<Result<{
144
+ readonly revision: string; readonly records: readonly {
145
+ readonly issuer: string; readonly subject: string; readonly record: ProviderDirectoryRecord | null;
146
+ }[];
147
+ }>>>;
148
+ }
149
+ /** Core implements these host-owned operations, not IdentityHost's fallback/directory composition. */
150
+ export interface ForkIdentityProtocolV1<RequestContext, ToolContext, Input> {
151
+ readonly version: 1;
152
+ /** Persisted core storage namespace; stable across plugin replacement and never request-derived. */
153
+ readonly instanceId: string;
154
+ /** Enables this plugin generation's producer-owned external message rendering in the SDK host. */
155
+ experimental_useProducerMessageRendering?(): void;
156
+ /** /bb installs exactly the returned handler through native RPC/HTTP registration after schema
157
+ * validation. Core supplies the request-scoped context and its lifecycle signal out of band.
158
+ */
159
+ bindInvocation<Args extends readonly unknown[], Output>(
160
+ input: { readonly routeClass: ForkInvocationRouteClass;
161
+ readonly handler: (context: ForkInvocationContext<RequestContext>, ...args: Args) => Output }
162
+ ): { readonly registration: ForkInvocationRegistration; readonly handler: (...args: Args) => Output };
163
+ session(context: RequestContext): Promise<Wire<ServerSession>>;
164
+ selfProfile(context: RequestContext, options?: ReadOptions): Promise<Wire<Result<IdentityProfile>>>;
165
+ openRequest(context: RequestContext): Promise<Result<ForkRequestHandle>>;
166
+ /** Checks source scope/live request during durable acceptance; retained receipts remain queryable after the request ends. */
167
+ accept(input: ForkAcceptanceRequest<Input>, options?: ReadOptions): Promise<Wire<AcceptanceOutcome>>;
168
+ lookup(operationId: string, options?: ReadOptions): Promise<Wire<Result<OperationLookup<AcceptanceOutcome>>>>;
169
+ provenance(context: ToolContext): Promise<Wire<Result<ExecutionProvenance>>>;
170
+ historyContributions(query: Wire<import('./model.js').ContributionQuery>, options?: ReadOptions): Promise<Wire<Result<import('./model.js').EvidencePage<Contribution>>>>;
171
+ historyAttempts(query: Wire<import('./model.js').AttemptQuery>, options?: ReadOptions): Promise<Wire<Result<import('./model.js').EvidencePage<ExecutionProvenance>>>>;
172
+ /** Borrowed provider callbacks; package composes/searches/normalizes them with default/external profiles. */
173
+ directorySources(): readonly ProviderDirectorySource[];
174
+ participants(input: Wire<import('./model.js').ParticipantQuery>, options?: ReadOptions): Promise<Wire<Awaited<ReturnType<ParticipantReader['list']>>>>;
175
+ /** RPC-only forwarding fences the original scope and destination generation before and after work. */
176
+ forwardRpc(scope: ForkInvocationScope, destination: { readonly pluginId: string; readonly method: string },
177
+ input: import('./model.js').Json): Promise<unknown>;
178
+ registerProvider(provider: ForkIdentityProvider): Promise<Result<ForkProviderRegistration>>;
179
+ subscribe(listener: (event: HostInvalidation) => void): Unsubscribe;
180
+ }
181
+ export type ExtensionDiscovery<R, T, I> =
182
+ | { readonly status: 'absent' }
183
+ | { readonly status: 'supported'; readonly protocol: ForkIdentityProtocolV1<R, T, I> }
184
+ | { readonly status: 'unsupported-version'; readonly version: number }
185
+ | { readonly status: 'malformed'; readonly error: IdentityError };
186
+ /** The only optional fork discovery member. It is read from a trusted SDK host object, never browser data. */
187
+ export interface ForkIdentityExtensionSurface { readonly experimental_p6rIdentity?: unknown; }
188
+ /** Runs against trusted experimental_p6rIdentity property value, never a browser payload. */
189
+ export declare function inspectForkExtension<R, T, I>(value: unknown): ExtensionDiscovery<R, T, I>;
190
+ export declare function createHostAdapter<R, T, I>(options: {
191
+ readonly upstream: UpstreamDriver<R, T, I>;
192
+ readonly extension: ExtensionDiscovery<R, T, I>;
193
+ readonly directory?: Directory;
194
+ }): IdentityHost<R, T, I>;
195
+
196
+ /** Operator-owned selection; supplied by host configuration, never by request payload or provider.
197
+ * Credential names are allowlisted here; providers receive only this boundary's selected evidence.
198
+ */
199
+ export interface ProviderBoundaryConfigurationV1 {
200
+ readonly version: 1; readonly boundaryId: string; readonly pluginId: string;
201
+ readonly ingressIds: readonly string[];
202
+ readonly credentials: readonly { readonly name: string; readonly source: 'header' | 'cookie'; readonly field: string }[];
203
+ readonly resolver: { readonly timeoutMs: number };
204
+ }
205
+ /** In-process envelope: credentials await provider verification, transport facts are host-established.
206
+ * Never log/persist credentials in profiles, bootstrap, provenance, or generic event payloads.
207
+ */
208
+ export interface ProviderEvidenceV1 {
209
+ readonly version: 1; readonly configuration: ProviderBoundaryConfigurationV1;
210
+ readonly request: { readonly authority: string; readonly method: string; readonly pathname: string;
211
+ readonly transport: 'http' | 'websocket'; readonly receivedAt: number };
212
+ readonly ingress: { readonly id: string | null; readonly kind: 'owned-proxy' | 'local' | 'unverified';
213
+ readonly authenticatedPeer: string | null };
214
+ readonly credentials: readonly { readonly name: string; readonly value: string }[];
215
+ readonly deadlineAt: number;
216
+ readonly signal: AbortSignal;
217
+ }
218
+ export interface ProviderDirectoryRecord {
219
+ readonly issuer: string; readonly subject: string; readonly presentation: ProfilePresentation;
220
+ readonly status: 'current' | 'historical';
221
+ }
222
+ export interface IdentityProvider {
223
+ readonly issuers: readonly IssuerId[];
224
+ /** Optional provider-local readiness. Host structural/configuration checks are mandatory.
225
+ * It receives no request evidence or credentials and cannot resolve an actor.
226
+ */
227
+ readonly validateReadiness?: (input: { readonly generation: ProviderGeneration;
228
+ readonly configuration: Readonly<ProviderBoundaryConfigurationV1>;
229
+ readonly deadlineAt: number; readonly signal: AbortSignal }) => Promise<Result<void>>;
230
+ resolve(evidence: ProviderEvidenceV1): Promise<
231
+ | { readonly status: 'resolved'; readonly issuer: string; readonly subject: string; readonly presentation: ProfilePresentation }
232
+ | { readonly status: 'not-applicable' }
233
+ | { readonly status: 'rejected' | 'unavailable'; readonly reason: string }
234
+ >;
235
+ /** Issuer/subject records are normalized into keys by the package; providers do not invent codecs. */
236
+ directory?(input: { readonly query: string; readonly cursor?: string; readonly limit: number; readonly history: 'current' | 'include-historical' }, options?: ReadOptions): Promise<Result<{
237
+ readonly records: readonly ProviderDirectoryRecord[]; readonly nextCursor: string | null; readonly revision: string;
238
+ }>>;
239
+ /** Revisions/cursors belong to a provider generation and directory snapshot. Stale cursors fail
240
+ * explicitly; outages are errors, never empty/missing. Reject undeclared issuer records.
241
+ */
242
+ lookup?(subjects: readonly { readonly issuer: string; readonly subject: string }[], options?: ReadOptions): Promise<Result<{
243
+ readonly revision: string; readonly records: readonly {
244
+ readonly issuer: string; readonly subject: string; readonly record: ProviderDirectoryRecord | null;
245
+ }[] }>>;
246
+ }
247
+ export interface ProviderDirectorySource {
248
+ readonly issuers: readonly string[];
249
+ readonly generation: string;
250
+ readonly directory?: NonNullable<IdentityProvider['directory']>;
251
+ readonly lookup?: NonNullable<IdentityProvider['lookup']>;
252
+ person(issuer: string, subject: string): Wire<Result<PersonReference>>;
253
+ }
254
+ /** Atomic activation belongs to the enhanced host; failure retains old generation, sockets reauthenticate.
255
+ * Disposal is generation-scoped. Provider removal yields unavailable, never absent-capability singleton mode.
256
+ */
257
+ /** Package-normalized registration; request lifecycle remains separate from provider state. */
258
+ export interface ProviderRegistrationV1 extends Disposable {
259
+ readonly generation: ProviderGeneration;
260
+ /** Registration during factory execution stages a candidate, it does not wait for activation.
261
+ * Host commits replacement only after plugin activation succeeds; failed candidates are cleaned up
262
+ * while prior provider resources remain live. Stale disposal/invalidation cannot affect a successor.
263
+ */
264
+ getStatus(): 'staged' | 'active' | 'retired';
265
+ /** Read-only non-secret configuration captured for this generation's readiness validation. */
266
+ readonly configuration: Readonly<ProviderBoundaryConfigurationV1>;
267
+ readonly signal: AbortSignal;
268
+ subscribe(listener: (status: 'staged' | 'active' | 'retired') => void): Unsubscribe;
269
+ /** Authentication invalidation updates provider enrichment; it does not by itself block ordinary work.
270
+ * Directory changes do not alone change actors.
271
+ */
272
+ invalidate(change: { readonly kind: 'authentication'; readonly subjects?: readonly { readonly issuer: string; readonly subject: string }[] }
273
+ | { readonly kind: 'directory'; readonly revision: string }): Result<void>;
274
+ person(issuer: IssuerId, subject: string): Result<PersonReference>;
275
+ /** Removes only this generation's registration when it is still current; never retires the plugin generation. */
276
+ dispose(): void;
277
+ }
package/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ /** @phosphorco/bb-identity: portable values only. Runtime-specific APIs use explicit subpaths. */
2
+ export type * from './model.js';
3
+ export { idCodec, newOperationId, identityKeyCodec, profileCodec, provenanceCodec, defaultIdentityKey } from './model.js';
package/model.d.ts ADDED
@@ -0,0 +1,231 @@
1
+ /** Shared serializable vocabulary. No React, BB SDK, storage, or provider imports. */
2
+ declare const identityKey: unique symbol;
3
+ declare const opaqueId: unique symbol;
4
+ export type IdentityKey = string & { readonly [identityKey]: true };
5
+ export type Id<Kind extends string> = string & { readonly [opaqueId]: Kind };
6
+ export type InstanceId = Id<'instance'>;
7
+ export type PluginId = Id<'plugin'>;
8
+ export type ThreadId = Id<'thread'>;
9
+ export type ContributionId = Id<'contribution'>;
10
+ export type OperationId = Id<'operation'>;
11
+ export type Revision = Id<'revision'>;
12
+ export type Cursor = Id<'cursor'>;
13
+ export type OwnerSessionId = Id<'owner-session'>;
14
+ /** Identifies one reviewed divergence within an owner-controller incarnation. */
15
+ export type ConflictToken = Id<'conflict'>;
16
+ /** Host-issued opaque provider generation; it is not activation authority. */
17
+ export type ProviderGeneration = Id<'provider-generation'>;
18
+ /** Issuers are stable provider identifiers; canonical identity construction remains host-owned. */
19
+ export type IssuerId = string;
20
+ export type SessionStamp = Id<'server-session'>;
21
+ /** Freshness assertion only; not a credential. Rotates on actor/provider discontinuity, including A→B→A. */
22
+ export interface RequestExpectation { readonly actor: IdentityKey; readonly session: SessionStamp; }
23
+ export type TargetSelection = { readonly kind: 'self' } | { readonly kind: 'person'; readonly key: IdentityKey };
24
+ /** Injected by bindings/tests; no hidden clock in timed primitives. */
25
+ export interface Scheduler {
26
+ now(): number;
27
+ schedule(delayMs: number, callback: () => void): Unsubscribe;
28
+ }
29
+ /** Serializable counterpart for a structural host protocol; preserves literal discriminants. */
30
+ export type Wire<T> = T extends IdentityKey | Id<string> ? string
31
+ : T extends readonly (infer V)[] ? readonly Wire<V>[]
32
+ : T extends object ? { readonly [K in keyof T]: Wire<T[K]> } : T;
33
+
34
+ export type Json = null | boolean | number | string | readonly Json[] | { readonly [key: string]: Json };
35
+ export type Unsubscribe = () => void;
36
+ export interface Disposable { dispose(): void; }
37
+ export interface ReadOptions { readonly signal?: AbortSignal; }
38
+
39
+ /** Expected failures are values. Implementations may still throw programmer errors. */
40
+ export interface IdentityError {
41
+ readonly code: 'unavailable' | 'unauthenticated' | 'unsupported' | 'incompatible'
42
+ | 'invalid-input' | 'not-found' | 'ambiguous' | 'stale-owner' | 'stale-context'
43
+ | 'conflict' | 'cancelled' | 'disposed' | 'limit-exceeded' | 'invalid-operation' | 'expired';
44
+ readonly message: string;
45
+ readonly retry: 'never' | 'after-refresh' | 'after-reconnect' | 'same-operation';
46
+ }
47
+ export type Result<T> = { readonly ok: true; readonly value: T }
48
+ | { readonly ok: false; readonly error: IdentityError };
49
+
50
+ /** Codecs validate bounds/canonical form at runtime; casts are not validation. */
51
+ export interface Codec<T> {
52
+ decode(input: unknown): Result<T>;
53
+ encode(value: T): Json;
54
+ }
55
+ /** Validates a bounded ID at an SDK/wire boundary; IDs are never authority tokens. */
56
+ export declare function idCodec<Kind extends string>(kind: Kind): Codec<Id<Kind>>;
57
+ export declare function newOperationId(): OperationId;
58
+ export declare const identityKeyCodec: Codec<IdentityKey>;
59
+ export declare const profileCodec: Codec<IdentityProfile>;
60
+ export declare const provenanceCodec: Codec<ExecutionProvenance>;
61
+ /** Singleton belongs to host state, not the browser or package installation. */
62
+ export declare function defaultIdentityKey(instance: InstanceId): IdentityKey;
63
+
64
+ export interface ProfilePresentation {
65
+ readonly displayName: string;
66
+ readonly handle: string | null;
67
+ readonly avatarUrl: string | null;
68
+ }
69
+ export type IdentityReference =
70
+ | { readonly kind: 'person'; readonly key: IdentityKey; readonly issuer: string; readonly subject: string }
71
+ | { readonly kind: 'default-user'; readonly key: IdentityKey; readonly instanceId: InstanceId }
72
+ | { readonly kind: 'machine'; readonly key: IdentityKey; readonly instanceId: InstanceId; readonly hostId: string | null }
73
+ | { readonly kind: 'external'; readonly key: IdentityKey; readonly pluginId: PluginId; readonly subject: string };
74
+ /** Human/default identities remain valid person selections; machine actors are separate. */
75
+ export type PersonReference = Extract<IdentityReference, { kind: 'person' | 'default-user' }>;
76
+ /** An actor that can own a request or self-scoped state, including machine fallback. */
77
+ export type ActorReference = PersonReference | Extract<IdentityReference, { kind: 'machine' }>;
78
+ export type MachineReference = Extract<IdentityReference, { kind: 'machine' }>;
79
+ export interface IdentityProfile {
80
+ readonly identity: IdentityReference;
81
+ readonly presentation: ProfilePresentation;
82
+ readonly revision: Revision;
83
+ readonly status: 'current' | 'historical';
84
+ }
85
+ export interface ActorSnapshot {
86
+ readonly identity: IdentityReference;
87
+ readonly presentation: ProfilePresentation;
88
+ /** Historical evidence, never permission to perform a later action. */
89
+ readonly evidence: 'provider-verified' | 'local-user' | 'upstream-default' | 'integration-asserted' | 'legacy' | 'machine';
90
+ }
91
+ export type Origin =
92
+ | { readonly kind: 'person'; readonly actor: ActorSnapshot & { readonly identity: PersonReference } }
93
+ | { readonly kind: 'machine'; readonly actor: ActorSnapshot & { readonly identity: MachineReference } }
94
+ | { readonly kind: 'external'; readonly actor: ActorSnapshot & { readonly identity: Extract<IdentityReference, { kind: 'external' }> } }
95
+ | { readonly kind: 'agent'; readonly agentId: string | null }
96
+ | { readonly kind: 'system'; readonly reason: string }
97
+ | { readonly kind: 'unknown'; readonly reason: 'legacy' | 'upstream-unattributed' | 'missing-source' };
98
+ export interface ContributionReference {
99
+ readonly threadId: ThreadId;
100
+ readonly contributionId: ContributionId;
101
+ }
102
+ /** A contribution is independent of delivery grouping and execution attempts. */
103
+ export interface Contribution {
104
+ readonly reference: ContributionReference;
105
+ readonly author: Origin;
106
+ readonly latestEditor: ActorSnapshot | null;
107
+ readonly acceptedAt: string;
108
+ readonly mentionedPeople: readonly PersonReference[];
109
+ }
110
+ export type AttemptInputSource =
111
+ | { readonly kind: 'contribution'; readonly reference: ContributionReference }
112
+ | { readonly kind: 'interaction'; readonly interactionId: string; readonly resolver: Origin }
113
+ | { readonly kind: 'generated'; readonly purpose: 'continuation' | 'resolved-resource' | 'system-context';
114
+ readonly basedOn: readonly ContributionReference[] };
115
+ /** Ordered delivery groups; an individual group can contain multiple original/generated sources. */
116
+ export interface AttemptInputGroup { readonly sources: readonly AttemptInputSource[]; }
117
+ export interface ExecutionCorrelation {
118
+ readonly threadId: ThreadId;
119
+ readonly turnId: string;
120
+ readonly attemptId: string;
121
+ readonly toolCallId: string | null;
122
+ }
123
+ /** Native identifiers are opaque; lookup never guesses from text or the latest author. */
124
+ export type ContributionQuery = (
125
+ | { readonly kind: 'references'; readonly references: readonly ContributionReference[] }
126
+ | { readonly kind: 'native-message'; readonly threadId: ThreadId; readonly messageId: string }
127
+ | { readonly kind: 'native-event'; readonly threadId: ThreadId; readonly eventId: string }
128
+ ) & {
129
+ /** Cursor is bound to this exact query and retained-history snapshot. */
130
+ readonly cursor?: Cursor;
131
+ readonly limit?: number;
132
+ };
133
+ export type EvidencePage<T> =
134
+ | { readonly status: 'known' | 'partial'; readonly items: readonly T[]; readonly nextCursor: Cursor | null;
135
+ /** Traversal completeness is distinct from known/partial evidence. */
136
+ readonly traversal: 'complete' | 'continued'; readonly missing: readonly string[] }
137
+ | { readonly status: 'pending' }
138
+ | { readonly status: 'unavailable'; readonly reason: 'unsupported' | 'missing-history' | 'outage' };
139
+ export type AttemptQuery = ({ readonly kind: 'operation'; readonly operationId: OperationId }
140
+ | { readonly kind: 'contribution'; readonly reference: ContributionReference }) & {
141
+ readonly cursor?: Cursor; readonly limit?: number;
142
+ };
143
+ /** Bounded reads: 100 references, default 50/max 200 attempts. Known has no missing facts.
144
+ * Each attempt preserves execution-time snapshots. A queued operation may be pending;
145
+ * a known empty result is authoritative only within retained coverage, never inferred upstream.
146
+ */
147
+ export interface ProvenanceReader {
148
+ contributions(query: ContributionQuery, options?: ReadOptions): Promise<Result<EvidencePage<Contribution>>>;
149
+ attempts(query: AttemptQuery, options?: ReadOptions): Promise<Result<EvidencePage<ExecutionProvenance>>>;
150
+ }
151
+ /** Snapshots are fixed when the attempt accepts input, unaffected by later edits.
152
+ * Known: every referenced contribution appears exactly once in contributions, with no unresolved causal source.
153
+ * Partial: missing references are explicit. Consumers never choose the first/latest author as a substitute.
154
+ */
155
+ export type ExecutionProvenance =
156
+ | { readonly status: 'known'; readonly correlation: ExecutionCorrelation;
157
+ readonly inputGroups: readonly AttemptInputGroup[]; readonly contributions: readonly Contribution[] }
158
+ | { readonly status: 'partial'; readonly correlation: ExecutionCorrelation | null;
159
+ readonly inputGroups: readonly AttemptInputGroup[]; readonly contributions: readonly Contribution[];
160
+ readonly missing: readonly ContributionReference[]; readonly reason: string }
161
+ | { readonly status: 'unknown'; readonly correlation: ExecutionCorrelation | null; readonly reason: string };
162
+ export interface Page<T> {
163
+ readonly items: readonly T[];
164
+ readonly nextCursor: Cursor | null;
165
+ /** Cursors are bound to the same query and snapshot; a stale cursor errors. */
166
+ readonly revision: Revision;
167
+ }
168
+ export interface PageRequest { readonly cursor?: Cursor; readonly limit: number; }
169
+ export interface DirectoryQuery extends PageRequest {
170
+ readonly query: string; readonly kinds: readonly IdentityReference['kind'][];
171
+ readonly history: 'current' | 'include-historical';
172
+ }
173
+ export interface ProfileQuery { readonly keys: readonly IdentityKey[]; }
174
+ export interface ParticipantQuery extends PageRequest { readonly threadId: ThreadId; }
175
+ export interface ParticipantPreviewQuery { readonly threadIds: readonly ThreadId[]; readonly perThreadLimit: number; }
176
+ export interface Directory {
177
+ search(input: DirectoryQuery, options?: ReadOptions): Promise<Result<Page<IdentityProfile>>>;
178
+ getMany(input: ProfileQuery, options?: ReadOptions): Promise<Result<readonly ProfileLookup[]>>;
179
+ /** Invalidations, not an unbounded global roster broadcast. */
180
+ subscribe(keys: readonly IdentityKey[], listener: (event: DirectoryInvalidation) => void): Unsubscribe;
181
+ }
182
+ export type ProfileLookup = { readonly key: IdentityKey; readonly status: 'found'; readonly profile: IdentityProfile }
183
+ | { readonly key: IdentityKey; readonly status: 'missing' };
184
+ /** `null` is an explicit conservative reset when the producer has no authoritative directory revision. */
185
+ export interface DirectoryInvalidation { readonly keys: readonly IdentityKey[]; readonly revision: Revision | null; }
186
+ export interface Participant {
187
+ readonly identity: IdentityReference;
188
+ readonly presentation: ProfilePresentation;
189
+ readonly roles: readonly ('author' | 'editor' | 'mentioned' | 'interaction-resolver')[];
190
+ }
191
+ /** Deduplicated people/external identities from retained visible durable history; aggregates roles.
192
+ * Excludes queue-only drafts. Mentioned-only identities need no fabricated authorship evidence.
193
+ */
194
+ export interface ParticipantReader {
195
+ list(input: ParticipantQuery, options?: ReadOptions): Promise<Result<Page<Participant> & { readonly coverage: 'complete-history' | 'partial-history' }>>;
196
+ /** Bounded preview per thread; hasMore prevents interpreting truncation as absence. */
197
+ previews(input: ParticipantPreviewQuery, options?: ReadOptions): Promise<Result<readonly {
198
+ readonly threadId: ThreadId; readonly participants: readonly Participant[]; readonly hasMore: boolean;
199
+ readonly coverage: 'complete-history' | 'partial-history'; readonly revision: Revision;
200
+ }[]>>;
201
+ }
202
+
203
+ export interface NativeSubmissionIds {
204
+ readonly deliveryId: string | null; readonly queuedMessageId: string | null;
205
+ readonly turnId: string | null;
206
+ }
207
+ export type AcceptanceReceipt =
208
+ | { readonly evidence: 'host-accepted'; readonly operationId: OperationId; readonly acceptedAt: string;
209
+ readonly references: readonly ContributionReference[]; readonly native: NativeSubmissionIds;
210
+ readonly provenance: 'structured'; readonly deduplication: 'guaranteed'; readonly retainedUntil: string }
211
+ | { readonly evidence: 'upstream-response'; readonly operationId: OperationId;
212
+ readonly acceptedAt: null; readonly references: null; readonly native: NativeSubmissionIds;
213
+ readonly provenance: 'source-labelled' | 'upstream-default'; readonly deduplication: 'not-guaranteed' };
214
+ /** Upstream successful submission does not fabricate exact native acceptance evidence. */
215
+ export type AcceptanceOutcome =
216
+ | { readonly status: 'submitted'; readonly receipt: AcceptanceReceipt }
217
+ | { readonly status: 'rejected'; readonly error: IdentityError }
218
+ | { readonly status: 'indeterminate'; readonly operationId: OperationId; readonly message: string };
219
+ export type OperationLookup<Outcome> =
220
+ | { readonly status: 'final'; readonly outcome: Outcome }
221
+ | { readonly status: 'pending' }
222
+ | { readonly status: 'absent-final'; readonly retry: 'same-operation-only' }
223
+ | { readonly status: 'unknown'; readonly reason: 'unsupported' | 'expired' | 'unavailable' };
224
+ export interface SendInput<Input> {
225
+ readonly operationId: OperationId;
226
+ readonly threadId: ThreadId;
227
+ readonly input: readonly Input[];
228
+ /** `auto` steers active work and starts idle work; it is never normalized by this package. */
229
+ readonly mode: 'auto' | 'start' | 'steer-if-active' | 'queue-if-active';
230
+ }
231
+ export interface PersonSendInput<Input> extends SendInput<Input> { readonly expected: RequestExpectation; }
package/package.json ADDED
@@ -0,0 +1,110 @@
1
+ {
2
+ "name": "@phosphorco/bb-identity",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "Portable identity, provenance, and state contracts for BB integrations.",
7
+ "author": "Phosphor",
8
+ "homepage": "https://github.com/phosphorco/bb-community-plugins/tree/main/packages/bb-identity#readme",
9
+ "bugs": {
10
+ "url": "https://github.com/phosphorco/bb-community-plugins/issues"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/phosphorco/bb-community-plugins.git",
15
+ "directory": "packages/bb-identity"
16
+ },
17
+ "keywords": [
18
+ "bb",
19
+ "identity",
20
+ "provenance",
21
+ "state"
22
+ ],
23
+ "exports": {
24
+ ".": {
25
+ "types": "./index.d.ts",
26
+ "import": "./dist/index-runtime.js"
27
+ },
28
+ "./model": {
29
+ "types": "./model.d.ts",
30
+ "import": "./dist/model-runtime.js"
31
+ },
32
+ "./host": {
33
+ "types": "./host.d.ts",
34
+ "import": "./dist/host-runtime.js"
35
+ },
36
+ "./server": {
37
+ "types": "./server.d.ts",
38
+ "import": "./dist/server-runtime.js"
39
+ },
40
+ "./testing": {
41
+ "types": "./testing.d.ts",
42
+ "import": "./dist/testing-runtime.js"
43
+ },
44
+ "./state": {
45
+ "types": "./state.d.ts",
46
+ "import": "./dist/state-runtime.js"
47
+ },
48
+ "./bb": {
49
+ "types": "./bb.d.ts",
50
+ "import": "./dist/bb-entry-runtime.js"
51
+ },
52
+ "./client": {
53
+ "types": "./client.d.ts",
54
+ "import": "./dist/client-entry-runtime.js"
55
+ },
56
+ "./react": {
57
+ "types": "./react.d.ts",
58
+ "import": "./dist/react-entry-runtime.js"
59
+ }
60
+ },
61
+ "files": [
62
+ "dist/",
63
+ "index.d.ts",
64
+ "model.d.ts",
65
+ "host.d.ts",
66
+ "server.d.ts",
67
+ "testing.d.ts",
68
+ "client.d.ts",
69
+ "state.d.ts",
70
+ "bb.d.ts",
71
+ "react.d.ts",
72
+ "README.md",
73
+ "LICENSE"
74
+ ],
75
+ "publishConfig": {
76
+ "access": "public"
77
+ },
78
+ "dependencies": {
79
+ "zod": "4.3.6"
80
+ },
81
+ "peerDependencies": {
82
+ "@get-bb/plugin-sdk": "^0.4.15 || ^0.5.9",
83
+ "react": "^19.0.0"
84
+ },
85
+ "peerDependenciesMeta": {
86
+ "@get-bb/plugin-sdk": {
87
+ "optional": true
88
+ },
89
+ "react": {
90
+ "optional": true
91
+ }
92
+ },
93
+ "devDependencies": {
94
+ "@get-bb/plugin-sdk": "0.5.29",
95
+ "@playwright/test": "1.63.0",
96
+ "@types/bun": "1.3.14",
97
+ "@types/react": "19.2.2",
98
+ "@types/react-dom": "19.2.2",
99
+ "bun": "1.3.14",
100
+ "react": "19.2.1",
101
+ "react-dom": "19.2.1",
102
+ "typescript": "^5.9.3"
103
+ },
104
+ "scripts": {
105
+ "build": "bun build index-runtime.ts --outdir dist --target bun --format esm --external \"react\" --external \"react/*\" --external \"react-dom\" --external \"react-dom/*\" --external \"@get-bb/plugin-sdk\" --external \"@get-bb/plugin-sdk/*\" --external \"zod\" && bun build model-runtime.ts --outdir dist --target bun --format esm --external \"react\" --external \"react/*\" --external \"react-dom\" --external \"react-dom/*\" --external \"@get-bb/plugin-sdk\" --external \"@get-bb/plugin-sdk/*\" --external \"zod\" && bun build host-runtime.ts --outdir dist --target bun --format esm --external \"react\" --external \"react/*\" --external \"react-dom\" --external \"react-dom/*\" --external \"@get-bb/plugin-sdk\" --external \"@get-bb/plugin-sdk/*\" --external \"zod\" && bun build server-runtime.ts --outdir dist --target bun --format esm --external \"react\" --external \"react/*\" --external \"react-dom\" --external \"react-dom/*\" --external \"@get-bb/plugin-sdk\" --external \"@get-bb/plugin-sdk/*\" --external \"zod\" && bun build testing-runtime.ts --outdir dist --target bun --format esm --external \"react\" --external \"react/*\" --external \"react-dom\" --external \"react-dom/*\" --external \"@get-bb/plugin-sdk\" --external \"@get-bb/plugin-sdk/*\" --external \"zod\" && bun build state-runtime.ts --outdir dist --target bun --format esm --external \"react\" --external \"react/*\" --external \"react-dom\" --external \"react-dom/*\" --external \"@get-bb/plugin-sdk\" --external \"@get-bb/plugin-sdk/*\" --external \"zod\" && bun build bb-entry-runtime.ts --outdir dist --target bun --format esm --external \"react\" --external \"react/*\" --external \"react-dom\" --external \"react-dom/*\" --external \"@get-bb/plugin-sdk\" --external \"@get-bb/plugin-sdk/*\" --external \"zod\" && bun build client-entry-runtime.ts --outdir dist --target browser --format esm --external \"react\" --external \"react/*\" --external \"react-dom\" --external \"react-dom/*\" --external \"@get-bb/plugin-sdk\" --external \"@get-bb/plugin-sdk/*\" --external \"zod\" && bun build react-entry-runtime.ts --outdir dist --target browser --format esm --external \"react\" --external \"react/*\" --external \"react-dom\" --external \"react-dom/*\" --external \"@get-bb/plugin-sdk\" --external \"@get-bb/plugin-sdk/*\" --external \"zod\" && bun build state-service-runtime.ts --outdir dist --target bun --format esm --external \"react\" --external \"react/*\" --external \"react-dom\" --external \"react-dom/*\" --external \"@get-bb/plugin-sdk\" --external \"@get-bb/plugin-sdk/*\" --external \"zod\" && bun build state-controller-runtime.ts --outdir dist --target bun --format esm --external \"react\" --external \"react/*\" --external \"react-dom\" --external \"react-dom/*\" --external \"@get-bb/plugin-sdk\" --external \"@get-bb/plugin-sdk/*\" --external \"zod\" && bun build state-transport-runtime.ts --outdir dist --target bun --format esm --external \"react\" --external \"react/*\" --external \"react-dom\" --external \"react-dom/*\" --external \"@get-bb/plugin-sdk\" --external \"@get-bb/plugin-sdk/*\" --external \"zod\"",
106
+ "test": "bun test ./type-tests/*.runtime.test.ts ./type-tests/*.runtime.test.mjs ./type-tests/client-runtime.test.mjs && node --experimental-strip-types --experimental-loader ./type-tests/source-ts-loader.mjs --test ./type-tests/portable-binding-acceptance.node.test.mjs",
107
+ "typecheck": "tsc --noEmit",
108
+ "browser-check": "PLAYWRIGHT_BROWSERS_PATH=0 bun ./type-tests/browser/run.mts"
109
+ }
110
+ }