@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/react.d.ts ADDED
@@ -0,0 +1,129 @@
1
+ /**
2
+ * React ownership, state hooks and shared normalized identity presentation.
3
+ * Feature conflict presentation and retry policy remain feature-owned.
4
+ */
5
+ import type { ComponentType, ReactNode } from 'react';
6
+ import type {
7
+ IdentityClient,
8
+ IdentityConnection,
9
+ IdentitySession,
10
+ IdentityStateBinding,
11
+ IdentityStateBindingOptions,
12
+ IdentityView,
13
+ PendingEditPolicy,
14
+ StateBindingSource,
15
+ ViewSnapshot,
16
+ } from './client.js';
17
+ import type { IdentityError, IdentityProfile, ProfilePresentation, Result } from './model.js';
18
+ import type { PendingDraft, SyncSnapshot } from './state.js';
19
+
20
+ export interface IdentityProviderProps {
21
+ /** Borrowed: Provider never starts or disposes this client. Null suspends
22
+ * children while retaining this mounted Provider's preservation/failure owner. */
23
+ readonly client: IdentityClient | null;
24
+ /** Rendered until a non-null client and the Provider owner are committed. */
25
+ readonly fallback?: ReactNode;
26
+ readonly children?: ReactNode;
27
+ }
28
+
29
+ export type IdentityContextProps = { readonly children?: ReactNode } & (
30
+ | { readonly view?: never; readonly pendingEdits?: PendingEditPolicy }
31
+ | { readonly view: IdentityView; readonly pendingEdits?: never }
32
+ );
33
+
34
+ /** Provider and Context establish ownership for the exported hooks/controls. */
35
+ export interface BbIdentityComponents {
36
+ readonly Provider: ComponentType<IdentityProviderProps>;
37
+ /** Borrows an injected view or owns a default view for this Context subtree. */
38
+ readonly Context: ComponentType<IdentityContextProps>;
39
+ }
40
+ export declare const BbIdentity: BbIdentityComponents;
41
+
42
+ /** Presentation is supplied by the caller; this component never fetches a profile. */
43
+ export interface IdentityAvatarProps {
44
+ readonly presentation: ProfilePresentation | null;
45
+ /** Human-readable fallback and accessible label; never pass an opaque identity key. */
46
+ readonly label: string;
47
+ readonly className?: string;
48
+ }
49
+ export declare const IdentityAvatar: ComponentType<IdentityAvatarProps>;
50
+
51
+ /** Compact normalized profile label. The fallback is caller-owned product wording. */
52
+ export interface IdentityLabelProps {
53
+ readonly profile: IdentityProfile | null;
54
+ readonly fallback: string;
55
+ readonly className?: string;
56
+ readonly primaryClassName?: string;
57
+ readonly secondaryClassName?: string;
58
+ }
59
+ export declare const IdentityLabel: ComponentType<IdentityLabelProps>;
60
+
61
+ export interface IdentityViewPickerLabels {
62
+ readonly trigger: string;
63
+ readonly self: string;
64
+ readonly viewingFallback: string;
65
+ readonly dialog: string;
66
+ readonly search: string;
67
+ readonly noResults: string;
68
+ readonly searchUnavailable: string;
69
+ }
70
+ export interface IdentityViewPickerProps {
71
+ /** Presentation only; selection always uses the normalized profile identity. */
72
+ readonly selectedProfile?: IdentityProfile | null;
73
+ /** Disables selecting a different view, but never the separate return-self control. */
74
+ readonly selectionDisabled?: boolean;
75
+ readonly labels: IdentityViewPickerLabels;
76
+ readonly className?: string;
77
+ readonly contentClassName?: string;
78
+ readonly searchClassName?: string;
79
+ readonly resultsClassName?: string;
80
+ readonly avatarClassName?: string;
81
+ /** Observes the same Result displayed by the control; feature retry policy stays local. */
82
+ readonly onResult?: (result: Result<void>) => void;
83
+ }
84
+ /** Uses the current Context view/client; no module-global client, view, or directory cache. */
85
+ export declare const IdentityViewPicker: ComponentType<IdentityViewPickerProps>;
86
+
87
+ export interface IdentityViewStatusLabels {
88
+ readonly viewing: string;
89
+ readonly returnSelf: string;
90
+ }
91
+ export interface IdentityViewStatusProps {
92
+ readonly labels: IdentityViewStatusLabels;
93
+ readonly className?: string;
94
+ /** Feature-specific selectors remain feature-owned. */
95
+ readonly returnSelfAttributes?: Readonly<Record<string, string>>;
96
+ readonly onResult?: (result: Result<void>) => void;
97
+ }
98
+ /** Shows an overriding-view status and its always-available, scope-fenced return action. */
99
+ export declare const IdentityViewStatus: ComponentType<IdentityViewStatusProps>;
100
+
101
+ /** Native SDK hook composition. Null means committed acquisition is still pending. */
102
+ export declare function useBbIdentityConnection(): IdentityConnection | null;
103
+ /** Native SDK hook composition. Null means the committed connection/client is not live yet. */
104
+ export declare function useBbIdentityClient(): IdentityClient | null;
105
+ /** Reads the explicitly borrowed Provider client; missing Provider is a setup error. */
106
+ export declare function useBorrowedIdentityClient(): IdentityClient;
107
+ export declare function useIdentitySession(): IdentitySession;
108
+
109
+ /** Framework-neutral binding ownership attached to the default Context view. */
110
+ export declare function useIdentityStateBinding<T>(
111
+ options: Omit<IdentityStateBindingOptions<T>, 'client' | 'view'> & StateBindingSource<T>,
112
+ ): IdentityStateBinding<T> | null;
113
+ /** Renderable snapshot for a nullable committed binding. */
114
+ export declare function useIdentityStateSnapshot<T>(binding: IdentityStateBinding<T> | null):
115
+ | SyncSnapshot<T>
116
+ | { readonly status: 'waiting-for-identity' };
117
+
118
+ export declare function useIdentityViewSnapshot(): ViewSnapshot;
119
+ export declare function useIdentityViewActions(): Pick<IdentityView, 'select' | 'reset'>;
120
+
121
+ /** Presentation-only failure record. The immutable generation acknowledges exactly one record. */
122
+ export interface IdentityPreservationFailure {
123
+ readonly generation: number;
124
+ readonly draft: PendingDraft<unknown>;
125
+ readonly error: IdentityError;
126
+ }
127
+ export declare function useIdentityPreservationFailures(): readonly IdentityPreservationFailure[];
128
+ /** Returns false for an absent or superseded generation; feature code owns retry/recovery. */
129
+ export declare function useAcknowledgeIdentityPreservationFailure(): (generation: number) => boolean;
package/server.d.ts ADDED
@@ -0,0 +1,80 @@
1
+ /** Request-bound server API; separate target intent, captured target facts, and live lifecycle validity. */
2
+ import type { AcceptanceOutcome, ActorReference, ActorSnapshot, Codec, Directory, Disposable, ExecutionProvenance,
3
+ IdentityKey, InstanceId, OperationId, OperationLookup, ParticipantReader, PersonReference,
4
+ PersonSendInput, ReadOptions, RequestExpectation, Result, SendInput, TargetSelection, Unsubscribe } from './model.js';
5
+ import type { ExternalAuthorInput, IdentityHost, IdentityCapabilities, ServerSession, HostInvalidation } from './host.js';
6
+
7
+ declare const resolvedTarget: unique symbol;
8
+ export interface TargetSnapshot {
9
+ readonly actor: ActorSnapshot & { readonly identity: ActorReference };
10
+ readonly subject: ActorReference;
11
+ readonly expected: RequestExpectation;
12
+ }
13
+ export interface ResolvedTarget<Intent extends 'read' | 'write'> {
14
+ readonly [resolvedTarget]: Intent;
15
+ readonly intent: Intent;
16
+ /** Reads frozen captured target facts for the commit checks. */
17
+ snapshot(): TargetSnapshot;
18
+ readonly signal: AbortSignal;
19
+ }
20
+ export interface CommitScope {
21
+ readonly instanceId: InstanceId;
22
+ readonly pluginId: import('./model.js').PluginId;
23
+ readonly collection: string; readonly recordId: string;
24
+ readonly subject: IdentityKey; readonly expected: RequestExpectation; readonly schemaVersion: number;
25
+ }
26
+ /** Validates write intent, captured actor/subject/session, address, namespace, and live lifecycle signal. */
27
+ export interface CommitValidator {
28
+ readonly boundary: 'same-process-synchronous';
29
+ validate(target: ResolvedTarget<'write'>, scope: CommitScope): Result<void>;
30
+ }
31
+ export type TargetPolicy = { readonly kind: 'self-only' } | { readonly kind: 'collaborators' };
32
+ export interface ReadTargetRequest {
33
+ readonly selection: TargetSelection; readonly policy: TargetPolicy; readonly intent: 'read';
34
+ }
35
+ export interface WriteTargetRequest {
36
+ readonly selection: TargetSelection; readonly policy: TargetPolicy; readonly intent: 'write';
37
+ readonly expected: RequestExpectation; readonly expectedSubject: IdentityKey;
38
+ }
39
+ export type TargetRequest = ReadTargetRequest | WriteTargetRequest;
40
+ export interface PersonRequest<Input> extends Disposable {
41
+ readonly actor: ActorSnapshot & { readonly identity: ActorReference };
42
+ readonly expected: RequestExpectation;
43
+ readonly capabilities: IdentityCapabilities;
44
+ readonly signal: AbortSignal;
45
+ target(input: ReadTargetRequest): Promise<Result<ResolvedTarget<'read'>>>;
46
+ target(input: WriteTargetRequest): Promise<Result<ResolvedTarget<'write'>>>;
47
+ send(input: PersonSendInput<Input>, options?: ReadOptions): Promise<AcceptanceOutcome>;
48
+ /** Host-bound on enhanced BB; upstream preserves the singleton convention with weaker lifetime guarantees. */
49
+ callPlugin<T>(input: { readonly pluginId: string; readonly method: string; readonly payload: import('./model.js').Json;
50
+ readonly output: Codec<T> }, options?: ReadOptions): Promise<Result<T>>;
51
+ }
52
+ export interface IdentityServer<RequestContext, ToolContext, Input> extends Disposable {
53
+ readonly instanceId: InstanceId;
54
+ readonly directory: Directory;
55
+ readonly participants: ParticipantReader;
56
+ readonly history: import('./model.js').ProvenanceReader;
57
+ readonly commits: CommitValidator;
58
+ session(context: RequestContext, options?: ReadOptions): Promise<ServerSession>;
59
+ selfProfile(context: RequestContext, options?: ReadOptions): Promise<Result<import('./model.js').IdentityProfile>>;
60
+ subscribe(listener: (event: HostInvalidation) => void): Unsubscribe;
61
+ personRequest(context: RequestContext, options?: ReadOptions): Promise<Result<PersonRequest<Input>>>;
62
+ toolProvenance(context: ToolContext): Promise<Result<ExecutionProvenance>>;
63
+ sendExternal(author: ExternalAuthorInput, input: SendInput<Input>, options?: ReadOptions): Promise<AcceptanceOutcome>;
64
+ /** Lookup reports retained operation outcomes; IDs are scoped to this instance/plugin. */
65
+ lookupOperation(operationId: OperationId, options?: ReadOptions): Promise<Result<OperationLookup<AcceptanceOutcome>>>;
66
+ }
67
+ export declare function createIdentityServer<R, T, I>(options: { readonly host: IdentityHost<R, T, I> }): IdentityServer<R, T, I>;
68
+ /** Wire query DTOs contain no AbortSignal. Options are local to this handler invocation. */
69
+ export interface IdentityEndpoint<RequestContext> extends Disposable {
70
+ bootstrap(context: RequestContext, options?: ReadOptions): Promise<Result<ServerSession>>;
71
+ selfProfile(context: RequestContext, options?: ReadOptions): Promise<Result<import('./model.js').IdentityProfile>>;
72
+ search(context: RequestContext, input: import('./model.js').DirectoryQuery, options?: ReadOptions): ReturnType<Directory['search']>;
73
+ profiles(context: RequestContext, input: import('./model.js').ProfileQuery, options?: ReadOptions): ReturnType<Directory['getMany']>;
74
+ participants(context: RequestContext, input: import('./model.js').ParticipantQuery, options?: ReadOptions): ReturnType<ParticipantReader['list']>;
75
+ participantPreviews(context: RequestContext, input: import('./model.js').ParticipantPreviewQuery, options?: ReadOptions): ReturnType<ParticipantReader['previews']>;
76
+ }
77
+ export declare function createIdentityEndpoint<R, T, I>(options: {
78
+ readonly server: IdentityServer<R, T, I>;
79
+ readonly publish: (event: import('./client.js').ClientInvalidation) => void;
80
+ }): IdentityEndpoint<R>;
package/state.d.ts ADDED
@@ -0,0 +1,170 @@
1
+ /** Framework-neutral synchronization; feature-owned schemas and transactional storage. */
2
+ import type { ActorSnapshot, Codec, ConflictToken, Disposable, IdentityKey, InstanceId, OperationId,
3
+ OperationLookup, OwnerSessionId, PluginId, ReadOptions, RequestExpectation, Result, Scheduler, Unsubscribe } from './model.js';
4
+ import type { CommitValidator, ResolvedTarget } from './server.js';
5
+ export interface StateAddress {
6
+ readonly instanceId: InstanceId; readonly pluginId: PluginId;
7
+ readonly collection: string; readonly recordId: string; readonly owner: IdentityKey;
8
+ }
9
+ /** Ordered only within an epoch. Reset/recreation produces a fresh epoch; empty records have stable versions. */
10
+ export interface StateVersion { readonly epoch: string; readonly sequence: number; }
11
+ export interface StateDefinition<T> {
12
+ readonly collection: string; readonly schemaVersion: number; readonly codec: Codec<T>;
13
+ initialValue(): T;
14
+ equal(left: T, right: T): boolean;
15
+ }
16
+ /** Share this feature-owned descriptor between client and server; address construction is package-owned. */
17
+ export interface StateResource<T> {
18
+ readonly pluginId: PluginId; readonly definition: StateDefinition<T>;
19
+ }
20
+ /** Borrows the same root-owned connection as identity; state never opens a competing feed. */
21
+ export declare function createStateTransport<T>(options: {
22
+ readonly connection: import('./client.js').IdentityConnection; readonly resource: StateResource<T>;
23
+ }): StateTransport<T>;
24
+ export interface StateEnvelope<T> {
25
+ readonly address: StateAddress; readonly version: StateVersion; readonly schemaVersion: number;
26
+ readonly value: T; readonly lastEditedBy: ActorSnapshot | null;
27
+ }
28
+ export type StateRead<T> =
29
+ | { readonly status: 'present'; readonly envelope: StateEnvelope<T> }
30
+ | { readonly status: 'empty'; readonly address: StateAddress; readonly version: StateVersion }
31
+ | { readonly status: 'migration-required'; readonly address: StateAddress; readonly storedSchemaVersion: number };
32
+ export interface StateMutation<T> {
33
+ /** Initialization never rebases defaults. An existing winner is adopted. */
34
+ readonly kind: 'initialize' | 'replace';
35
+ readonly address: StateAddress; readonly expectedVersion: StateVersion;
36
+ readonly expected: RequestExpectation; readonly ownerSession: OwnerSessionId;
37
+ readonly localGeneration: number; readonly operationId: OperationId;
38
+ readonly schemaVersion: number; readonly value: T;
39
+ }
40
+ export type StateOutcome<T> =
41
+ | { readonly status: 'saved' | 'unchanged'; readonly envelope: StateEnvelope<T>; readonly operationId: OperationId }
42
+ | { readonly status: 'already-initialized'; readonly current: StateRead<T>; readonly operationId: OperationId }
43
+ | { readonly status: 'conflict'; readonly current: StateRead<T>; readonly operationId: OperationId };
44
+ export type StateSave<T> = StateOutcome<T> | { readonly status: 'indeterminate'; readonly operationId: OperationId };
45
+ export interface StateInvalidation { readonly address: StateAddress; readonly version: StateVersion; readonly operationId: OperationId | null; }
46
+ export interface StateReadRequest { readonly address: StateAddress; readonly expected: RequestExpectation; }
47
+ export interface StateLookupRequest extends StateReadRequest { readonly operationId: OperationId; }
48
+ export interface StateTransport<T> {
49
+ load(input: StateReadRequest, options?: ReadOptions): Promise<Result<StateRead<T>>>;
50
+ save(input: StateMutation<T>, options?: ReadOptions): Promise<Result<StateSave<T>>>;
51
+ reconcile(input: StateLookupRequest, options?: ReadOptions): Promise<Result<OperationLookup<StateOutcome<T>>>>;
52
+ subscribe(address: StateAddress, listener: (event: StateInvalidation) => void): Unsubscribe;
53
+ }
54
+ /** Deduplicate immutable operation BEFORE CAS; replay exact original outcome even if current record advanced.
55
+ * Same ID with changed mutation fails invalid-operation. Record+receipt commit atomically.
56
+ * Retain receipts for receiptRetentionMs; expired IDs yield unknown, never permission to replace a pending op.
57
+ */
58
+ export interface AtomicStateStorage<T> {
59
+ readonly boundary: 'same-process-synchronous';
60
+ readonly receiptRetentionMs: number;
61
+ read(address: StateAddress, options?: ReadOptions): Promise<Result<StateRead<T>>>;
62
+ commit(input: {
63
+ readonly mutation: StateMutation<T>;
64
+ /** Invoke inside the synchronous transaction immediately before mutation; no await through commit. */
65
+ readonly validateAtCommit: () => Result<ActorSnapshot>;
66
+ }): Promise<Result<StateSave<T>>>;
67
+ reconcile(input: { readonly address: StateAddress; readonly operationId: OperationId }): Promise<Result<OperationLookup<StateOutcome<T>>>>;
68
+ }
69
+ export interface StateService<T> {
70
+ read(target: ResolvedTarget<'read'> | ResolvedTarget<'write'>, recordId: string, options?: ReadOptions): Promise<Result<StateRead<T>>>;
71
+ save(target: ResolvedTarget<'write'>, mutation: StateMutation<T>, options?: ReadOptions): Promise<Result<StateSave<T>>>;
72
+ reconcile(target: ResolvedTarget<'read'> | ResolvedTarget<'write'>, recordId: string, operationId: OperationId): Promise<Result<OperationLookup<StateOutcome<T>>>>;
73
+ }
74
+ export declare function createStateService<T>(options: {
75
+ readonly instanceId: InstanceId; readonly pluginId: PluginId; readonly definition: StateDefinition<T>;
76
+ readonly storage: AtomicStateStorage<T>; readonly commits: CommitValidator;
77
+ readonly publish: (event: StateInvalidation) => void;
78
+ }): StateService<T>;
79
+ /** Decode wrapper metadata as well as the feature value; local signals are never part of the wire codec. */
80
+ export declare function stateCodecs<T>(value: Codec<T>): {
81
+ readonly read: Codec<StateRead<T>>; readonly mutation: Codec<StateMutation<T>>;
82
+ readonly save: Codec<StateSave<T>>; readonly invalidation: Codec<StateInvalidation>;
83
+ readonly lookup: Codec<OperationLookup<StateOutcome<T>>>;
84
+ };
85
+
86
+ export interface DraftKey { readonly address: StateAddress; readonly actor: IdentityKey; readonly ownerSession: OwnerSessionId; }
87
+ export interface PendingDraft<T> {
88
+ readonly formatVersion: 1; readonly key: DraftKey; readonly schemaVersion: number;
89
+ readonly acknowledged: Exclude<StateRead<T>, { status: 'migration-required' }>;
90
+ readonly desired: T; readonly localGeneration: number;
91
+ /** Exact immutable submitted mutation, separate from newer desired content. */
92
+ readonly inFlight: StateMutation<T> | null;
93
+ }
94
+ export interface DraftCheckpoint<T> { readonly revision: number; readonly draft: PendingDraft<T>; }
95
+ export interface DraftStorage<T> {
96
+ /** Bounded recovery lookup across prior controller incarnations; never silently choose between drafts. */
97
+ find(address: StateAddress, actor: IdentityKey, limit: number): Promise<Result<readonly DraftCheckpoint<T>[]>>;
98
+ write(draft: PendingDraft<T>, expectedRevision: number | null): Promise<Result<DraftCheckpoint<T>>>;
99
+ /** An obsolete controller cannot erase a newer checkpoint. */
100
+ remove(key: DraftKey, expectedRevision: number): Promise<Result<void>>;
101
+ }
102
+ export type ConflictDecision<T> =
103
+ | { readonly kind: 'rebase'; readonly value: T }
104
+ | { readonly kind: 'accept-remote' }
105
+ | { readonly kind: 'needs-review'; readonly reason: string };
106
+ export interface ConflictContext<T> {
107
+ readonly cause: 'save-conflict' | 'dirty-load' | 'reconnect' | 'epoch-change' | 'draft-recovery';
108
+ /** A conflict decision is valid only for this token and this controller incarnation. */
109
+ readonly token: ConflictToken; readonly ownerSession: OwnerSessionId;
110
+ readonly base: StateRead<T>; readonly local: T; readonly remote: StateRead<T>;
111
+ }
112
+ export type SyncSnapshot<T> =
113
+ | { readonly status: 'loading'; readonly address: StateAddress }
114
+ | { readonly status: 'ready'; readonly address: StateAddress; readonly ownerSession: OwnerSessionId;
115
+ readonly acknowledged: StateRead<T>; readonly desired: T; readonly dirty: boolean;
116
+ readonly saving: boolean; readonly localGeneration: number; readonly remotePending: boolean }
117
+ | { readonly status: 'blocked'; readonly address: StateAddress;
118
+ readonly reason: 'identity-unavailable' | 'owner-changed' | 'conflict' | 'indeterminate' | 'storage-error' | 'migration-required' | 'draft-recovery';
119
+ readonly draft: PendingDraft<T> | null; readonly conflict: ConflictContext<T> | null; readonly message: string }
120
+ | { readonly status: 'detached'; readonly address: StateAddress; readonly draft: PendingDraft<T> | null };
121
+ export interface IdentityState<T> extends Disposable {
122
+ getSnapshot(): SyncSnapshot<T>;
123
+ subscribe(listener: () => void): Unsubscribe;
124
+ start(): Promise<Result<void>>;
125
+ edit(value: T): Result<void>;
126
+ /** Reference-counted transition pause. Stops automatic dispatch, including work
127
+ * awaiting a checkpoint; explicit flush remains available. Release resumes scheduling. */
128
+ suspendAutomaticDispatch(): Unsubscribe;
129
+ /** Waits for the pending intent observed at invocation, with a 30-second scheduler bound.
130
+ * Timeout retains the exact uncertain operation; it cannot retract accepted work.
131
+ */
132
+ flush(): Promise<Result<void>>;
133
+ /** Refresh identity first. Every remote divergence while dirty invokes conflict policy with ORIGINAL base. */
134
+ /** Health notifications may reuse current recovery; explicit reconnects refresh authority. */
135
+ reconnect(options?: { readonly reuseInFlight?: boolean }): Promise<Result<void>>;
136
+ /** Rejects a token superseded by a later remote divergence, even in this same controller. */
137
+ resolveConflict(token: ConflictToken, decision: ConflictDecision<T>): Promise<Result<void>>;
138
+ /** Recovery selection is explicit; exact uncertain operations are reconciled before accepting/rebasing desired. */
139
+ recover(checkpoint: DraftCheckpoint<T>): Promise<Result<void>>;
140
+ /** Explicitly forget one exact recovery checkpoint. A newer revision cannot
141
+ * be erased by a stale recovery list. */
142
+ discardRecovery(checkpoint: DraftCheckpoint<T>): Promise<Result<void>>;
143
+ /** At most 20 candidates for this address AND actual actor, across prior incarnations.
144
+ * Store survives controller disposal; schema mismatch and another actor's draft require an explicit
145
+ * feature import, never automatic recovery. Reconcile old uncertain operations without re-authoring.
146
+ */
147
+ recoveryCandidates(options?: ReadOptions): Promise<Result<readonly DraftCheckpoint<T>[]>>;
148
+ checkpoint(): Promise<Result<DraftCheckpoint<T> | null>>;
149
+ /** Immediate: stop writes/timers and fence late callbacks. The returned
150
+ * in-memory draft is durable only when this controller was given DraftStorage. */
151
+ detach(reason: 'identity-invalidated' | 'view-change' | 'unmount'): PendingDraft<T> | null;
152
+ /** Fallible preservation is separate from unconditional detach/dispose. With
153
+ * no DraftStorage, preserve is deliberately in-session only. Flush is bounded;
154
+ * discard cannot recall accepted work. */
155
+ close(options: { readonly pending: 'flush' | 'preserve' | 'discard' }): Promise<Result<void>>;
156
+ }
157
+ export declare function createIdentityState<T>(options: {
158
+ readonly address: StateAddress; readonly expected: RequestExpectation; readonly ownerSession: OwnerSessionId;
159
+ readonly definition: StateDefinition<T>; readonly transport: StateTransport<T>; readonly drafts?: DraftStorage<T>;
160
+ readonly scheduler?: Scheduler; readonly debounceMs?: number;
161
+ /** Maximum same-operation resubmissions during one explicit flush after an absent-final lookup.
162
+ * Default 3; zero forbids resubmission. Unknown/pending outcomes never authorize a retry.
163
+ */
164
+ readonly retryLimit?: number;
165
+ readonly onConflict: (context: ConflictContext<T>) => ConflictDecision<T>;
166
+ readonly initializeEmpty: boolean;
167
+ /** Must return the currently observed server session, not a captured initial identity. */
168
+ readonly currentSession: () => Result<import('./host.js').ServerSession>;
169
+ }): IdentityState<T>;
170
+ export declare function stateAddressKey(address: StateAddress): string;
package/testing.d.ts ADDED
@@ -0,0 +1,202 @@
1
+ /** Testing entry point: production-adapter inputs and separately labelled normalized consumer fixtures. */
2
+ import type { ActorReference, ActorSnapshot, Codec, Contribution, ContributionId, ExecutionProvenance,
3
+ IdentityKey, OperationId, PersonReference, Result, Scheduler,
4
+ SessionStamp, Unsubscribe } from './model.js';
5
+ import type { IdentityHost, UpstreamDriver } from './host.js';
6
+ import type { ConnectionHealth, IdentityClientTransport, IdentityConnection, IdentityConnectionEvent } from './client.js';
7
+ import type { AtomicStateStorage, StateAddress, StateEnvelope, StateRead, StateResource, StateVersion } from './state.js';
8
+ import type { TargetPolicy } from './server.js';
9
+ export interface ManualClock extends Scheduler {
10
+ advance(milliseconds: number): Promise<void>;
11
+ flush(): Promise<void>;
12
+ readonly pendingTimers: number;
13
+ }
14
+ export interface Deferred<T> { readonly promise: Promise<T>; resolve(value: T): void; reject(reason: unknown): void; }
15
+ export declare function createManualClock(initialMilliseconds?: number): ManualClock;
16
+ export declare function deferred<T>(): Deferred<T>;
17
+ /** Person fixture is for multi-person scenarios. Singleton comes only from the instance's reserved default. */
18
+ export declare function personFixture(input: { readonly subject: string; readonly name: string }): ActorSnapshot & { readonly identity: PersonReference };
19
+ export interface FakeRequest { readonly id: string; readonly origin: 'interactive-user' | 'agent' | 'background' | 'external' | 'unauthenticated'; }
20
+ export interface FakeToolCall { readonly id: string; }
21
+ export interface Pause { readonly reached: Promise<void>; release(): void; }
22
+ export interface AdapterHarness<Input> {
23
+ readonly upstream: UpstreamDriver<FakeRequest, FakeToolCall, Input>;
24
+ readonly clock: ManualClock;
25
+ /** Production inspectForkExtension and createHostAdapter path, not a supplied normalized host. */
26
+ connect(): IdentityHost<FakeRequest, FakeToolCall, Input>;
27
+ setRawExtension(value: unknown): void;
28
+ /** @throws This adapter fixture does not model operation lookup controls. */
29
+ setLookup(status: 'supported' | 'unsupported' | 'pending' | 'absent-final' | 'expired' | 'unavailable'): void;
30
+ request(origin?: FakeRequest['origin']): FakeRequest;
31
+ /** Multi-user replacement advances the session stamp and invalidates every issued request; singleton rejects it. */
32
+ setActor(actor: ActorSnapshot & { readonly identity: ActorReference }): Result<void>;
33
+ setIdentityAvailability(status: 'ready' | 'unavailable' | 'unauthenticated'): void;
34
+ settle(request: FakeRequest): void;
35
+ /** @throws Streaming is not modeled by this adapter fixture. */
36
+ constructStreamingResponse(request: FakeRequest): void;
37
+ /** @throws Streaming is not modeled by this adapter fixture. */
38
+ finishStream(request: FakeRequest): void;
39
+ abort(request: FakeRequest): void;
40
+ replaceProvider(result: 'succeed' | 'fail'): void;
41
+ disconnect(): void;
42
+ reconnect(): void;
43
+ /** @throws Acceptance pause points are not modeled by this adapter fixture. */
44
+ pauseAcceptance(point: 'before-dispatch' | 'before-commit' | 'after-commit-before-response'): Pause;
45
+ /** @throws Response loss is not modeled by this adapter fixture. */
46
+ loseNextAcceptanceResponse(): void;
47
+ /** @throws Causal contribution/attempt controls are not modeled by this adapter fixture. */
48
+ beginAttempt(groups: readonly (readonly ContributionId[])[]): string;
49
+ editContribution(id: ContributionId, editor: ActorSnapshot): void;
50
+ continueAttempt(attemptId: string, reason: string): string;
51
+ toolCallForAttempt(attemptId: string): FakeToolCall;
52
+ /** @throws Causal contribution/attempt controls are not modeled by this adapter fixture. */
53
+ readonly accepted: readonly Contribution[];
54
+ readonly observations: { readonly submissions: number; readonly rawExtensionReads: number;
55
+ readonly profileReads: number; readonly participantReads: number; readonly subscriptions: number };
56
+ dispose(): void;
57
+ }
58
+ export declare function createAdapterHarness<Input>(options: {
59
+ readonly mode: 'single-user' | 'multi-user';
60
+ /** Plain fixture IDs are validated and branded inside the test runtime. */
61
+ readonly instanceId?: string; readonly pluginId?: string;
62
+ readonly inputCodec: Codec<Input>; readonly clock?: ManualClock;
63
+ }): AdapterHarness<Input>;
64
+ /** Unit-only shortcut, intentionally not evidence of host causal attribution correctness. */
65
+ export declare function consumerProvenanceFixture(provenance: ExecutionProvenance): FakeToolCall;
66
+ /** Raw connection controls exercise production decoder/health paths, not a pre-normalized client.
67
+ * `state` and `identity` health can change independently so a state-only outage is reproducible.
68
+ */
69
+ export interface ConnectionHarness {
70
+ readonly connection: IdentityConnection;
71
+ /** Exercises production createIdentityClientTransport and createStateTransport paths. */
72
+ connect(): IdentityClientTransport;
73
+ setHealth(health: ConnectionHealth): void;
74
+ replyNext(method: string, payload: unknown): void;
75
+ emit(event: IdentityConnectionEvent): void;
76
+ pauseNextRequest(method: string): Pause;
77
+ readonly observations: { readonly methods: readonly string[]; readonly disposeCalls: number };
78
+ }
79
+ export declare function createConnectionHarness(): ConnectionHarness;
80
+ export interface StateStorageHarness<T> {
81
+ readonly storage: AtomicStateStorage<T>;
82
+ seed(envelope: StateEnvelope<T>): void;
83
+ seedEmpty(address: StateAddress, version: StateVersion): void;
84
+ seedReadFailure(error: import('./model.js').IdentityError): void;
85
+ seedLegacy(address: StateAddress, schemaVersion: number): void;
86
+ readCommitted(address: StateAddress): StateEnvelope<T> | null;
87
+ /** Snapshot timing is explicit to reproduce stale successful loads. */
88
+ pauseNextRead(options: { readonly capture: 'before-pause' | 'after-release' }): Pause;
89
+ pauseNextCommit(): Pause;
90
+ loseNextCommitResponse(): void;
91
+ expireReceipt(operationId: OperationId): void;
92
+ readonly commits: readonly { readonly actor: IdentityKey; readonly address: StateAddress; readonly version: StateVersion }[];
93
+ }
94
+ export declare function createStateStorageHarness<T>(options: { readonly clock: ManualClock }): StateStorageHarness<T>;
95
+
96
+ /** The parts of `createFakePluginHost()` (from `@get-bb/plugin-sdk/testing`) the wire fake uses. */
97
+ /** Structural, so `/testing` declarations compile without the optional SDK peer installed. */
98
+ export interface WireFakeHost {
99
+ readonly bb: {
100
+ readonly pluginId: string;
101
+ onDispose(dispose: () => void | Promise<void>): void;
102
+ readonly rpc: { register(contract: never, handlers: never): unknown };
103
+ readonly realtime: { publish(channel: string, payload: never): unknown };
104
+ readonly sdk: unknown;
105
+ };
106
+ readonly harness: {
107
+ readonly behavior: { callRpc(method: string, input?: unknown): Promise<unknown> };
108
+ readonly inspection: { readonly pluginId: string };
109
+ };
110
+ }
111
+ export interface IdentityWireFakeOptions<T> {
112
+ /** A fresh SDK fake host for the feature plugin; the production binding registers on it. */
113
+ readonly host: WireFakeHost;
114
+ /** `single-user` runs the upstream singleton path; `multi-user` adds a switchable actor and directory. */
115
+ readonly mode: 'single-user' | 'multi-user';
116
+ /** The feature's shared state resource; its pluginId must be the host's. */
117
+ readonly resource: StateResource<T>;
118
+ readonly policy?: TargetPolicy;
119
+ readonly readPolicy?: TargetPolicy;
120
+ readonly instanceId?: string;
121
+ /** Initial multi-user actor; default subject `wire-fake-user`. */
122
+ readonly actor?: { readonly subject: string; readonly name: string };
123
+ readonly clock?: ManualClock;
124
+ }
125
+ /**
126
+ * Wire-level identity server for client and app tests. Handlers run the production
127
+ * `bindBbIdentity` binding, endpoint and state bridge on the SDK fake host, with
128
+ * state in `createStateStorageHarness`. Source conformance only, not native host proof.
129
+ */
130
+ export interface IdentityWireFake<T> {
131
+ /** `bb-identity.v1.*` handlers, shaped for SDK `testing/app` `renderSlot(…, { rpc })`. */
132
+ readonly rpc: Readonly<Record<string, (input: unknown) => Promise<unknown>>>;
133
+ /** The realtime channel identity clients subscribe to. */
134
+ readonly channel: 'bb-identity/v1';
135
+ /** Each server publication, JSON-normalized. Forward it to the rendered slot's `emitRealtime`. */
136
+ onRealtime(listener: (channel: string, payload: unknown) => void): Unsubscribe;
137
+ readonly host: WireFakeHost;
138
+ /** Committed state, versions, pauses and operation outcomes. */
139
+ readonly storage: StateStorageHarness<T>;
140
+ /** Current multi-user actor; null in single-user mode. */
141
+ readonly actor: (ActorSnapshot & { readonly identity: ActorReference }) | null;
142
+ /** Multi-user only: replace the actor. The session stamp advances and older requests are fenced. */
143
+ switchActor(subject: string): void;
144
+ /** Multi-user only: add a person the directory can search and look up. */
145
+ addPerson(person: { readonly subject: string; readonly name: string }): void;
146
+ /** The identity key of a multi-user person, for state addresses. */
147
+ personKey(subject: string): IdentityKey;
148
+ /** Another writer commits a new version (same value unless given) and the change is published. */
149
+ invalidateExternally(address: StateAddress, value?: T): StateEnvelope<T>;
150
+ /** The next `count` state loads fail as `unavailable` with retry `after-reconnect`. */
151
+ failNextLoads(count: number): void;
152
+ /** The next state save commits, then the caller sees a transport failure. */
153
+ loseNextSaveResponse(): void;
154
+ /** Hold every state load until release. */
155
+ holdLoads(): Pause;
156
+ /** Hold every state commit until release. */
157
+ holdSaves(): Pause;
158
+ /** Every identity RPC method called, in order. */
159
+ readonly calls: readonly string[];
160
+ /** Every state save input, JSON-normalized. */
161
+ readonly saveInputs: readonly unknown[];
162
+ committed(address: StateAddress): StateEnvelope<T> | null;
163
+ dispose(): void;
164
+ }
165
+ export declare function createIdentityWireFake<T>(options: IdentityWireFakeOptions<T>): IdentityWireFake<T>;
166
+
167
+ /** Test registrar shared by `node:test` and `bun:test`. */
168
+ export interface StateStorageConformanceTest {
169
+ (name: string, fn: () => Promise<void>): unknown;
170
+ skip?(name: string, fn: () => Promise<void>): unknown;
171
+ }
172
+ export interface OpenedStateStorage<T> {
173
+ readonly storage: AtomicStateStorage<T>;
174
+ /** Reopen the same persisted store, as after a process restart. */
175
+ reopen?(): Promise<AtomicStateStorage<T>> | AtomicStateStorage<T>;
176
+ /** True while a synchronous commit transaction is open, so the suite can prove validateAtCommit runs inside it. */
177
+ insideCommit?(): boolean;
178
+ close?(): Promise<void> | void;
179
+ }
180
+ export interface StateStorageConformanceOptions<T> {
181
+ readonly test: StateStorageConformanceTest;
182
+ /** Open the feature's real storage over a fresh, empty store. It must read time from `now`. */
183
+ open(input: { readonly now: () => number }): Promise<OpenedStateStorage<T>> | OpenedStateStorage<T>;
184
+ /** A state address for `recordId` in the feature's collection. */
185
+ address(recordId: string): StateAddress;
186
+ /** Two distinct feature values. */
187
+ readonly values: readonly [T, T];
188
+ readonly schemaVersion?: number;
189
+ /** Editor snapshot returned by validateAtCommit. */
190
+ readonly actor?: ActorSnapshot;
191
+ /** Test-name prefix; default `state storage conformance: `. */
192
+ readonly prefix?: string;
193
+ /** Record a known gap with its reason instead of running the check. */
194
+ readonly skip?: { readonly restart?: string };
195
+ }
196
+ /**
197
+ * Registers the AtomicStateStorage contract checks: stable empty versions, one
198
+ * initialize winner, unchanged and conflict outcomes, immutable address-scoped
199
+ * operation ids with exact replay, expiry tombstones, commit-time rejection that
200
+ * leaves no record or outcome, and restart durability.
201
+ */
202
+ export declare function defineStateStorageConformance<T>(options: StateStorageConformanceOptions<T>): void;