@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Phosphor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,465 @@
1
+ # @phosphorco/bb-identity — portable plugin identity and state
2
+
3
+ > Policy update — 2026-09-09: the approved [Identities and multiplayer ADR](../../../docs/adrs/2026-09-identities-and-multiplayer.md)
4
+ > governs this trusted shared deployment. Use verified people when available,
5
+ > applicable carried attribution next, and a stable machine actor otherwise;
6
+ > missing or failed person verification must not block ordinary operations.
7
+ > Never relabel fallback as a verified person or redirect pending personal-state
8
+ > writes to another owner. Independent access checks and data validation remain.
9
+ > Earlier rejection requirements below are superseded; versioned API descriptions
10
+ > and test receipts remain historical evidence, not proof of ADR implementation.
11
+
12
+
13
+ Plugin authors use this package for request identity, view-as, personal state,
14
+ external contributions and provenance on both single-user BB and the enhanced
15
+ fork. Public `/bb`, `/client`, and `/react` entries are generated and have packed
16
+ consumer tests. `/bb` provides native RPC/state and request-scoped HTTP registration;
17
+ `/react` exposes its declared hooks and Provider/Context.
18
+
19
+ Start with the [progressive authoring skill](../../../.agents/skills/bb-identity/SKILL.md),
20
+ then the relevant [consumer recipe](CONSUMERS.md), and [PACKAGING.md](PACKAGING.md) for
21
+ build/host verification. Existing exports and green local tests do not imply
22
+ that real provider composition, every feature workflow, or identical-artifact
23
+ base/fork deployment has passed. Historical design/review sections below explain
24
+ decisions; the closure ledger owns current completion claims.
25
+
26
+ The [master plan](../../../fork/plans/bb-fork-master-plan.md) governs the design.
27
+ All collaborators have equal information access. The package separates actor,
28
+ viewed subject and operation target without adding permission tiers. Prompt
29
+ Stacks, notification policy, sidebar models, bulk execution and a generic
30
+ migration engine are outside this package.
31
+
32
+ ## Start here: the problem and the review standard
33
+
34
+ This package lets a plugin author build one useful feature for ordinary
35
+ single-user BB and collaborative BB. The feature should not know whether its
36
+ current person came from Tailscale, another IdP, or the upstream default user.
37
+ The fork provides host-owned identity facts and authored operations; this
38
+ package supplies adapters and reusable server/state/UI mechanics; feature
39
+ plugins keep their own product logic and storage.
40
+
41
+ Two different plugin authors must be served:
42
+
43
+ - **Feature authors** bind BB once, use request identity and view-as primitives,
44
+ and supply their feature schema, persistence and conflict policy. They should
45
+ not implement identity discovery, request forwarding, session invalidation or
46
+ React identity lifecycle machinery themselves.
47
+ - **IdP authors** supply verification and optional directory integration for
48
+ their own issuer(s). They should not patch the feature plugins, invent BB's
49
+ canonical keys, or depend on a hard-coded Tailnet provider name.
50
+
51
+ Reviewers should begin with this README, follow the entry-point map, then trace
52
+ complete consumer workflows through the declarations and the actual public BB
53
+ SDK. The intended behavior below is a requirement to challenge, not evidence
54
+ that the declared functions can implement it. Prior review dispositions record
55
+ proposed fixes, not implementation acceptance. Typechecks do not prove feasibility.
56
+
57
+ A sufficient public API has a closed path: identify every supplied input, its
58
+ source, the callable operation, the result, the next consumer, and the owner of
59
+ cleanup/recovery. If the path requires an undeclared bridge, direct fork fields,
60
+ private SDK access, a type assertion hiding incompatibility, or substantial
61
+ identity glue in a feature, identify that as a gap. Also call out public types
62
+ and responsibilities an ordinary author should never have to understand.
63
+
64
+ Feature authors should use the task-specific consumer recipe and its public
65
+ declarations. Adapter reviewers additionally inspect `/host`; test and release
66
+ owners use `/testing` and [PACKAGING.md](PACKAGING.md). The
67
+ [master plan](../../../fork/plans/bb-fork-master-plan.md) supplies constraints and history, not hidden
68
+ API definitions needed to use the package.
69
+
70
+ ## Review history
71
+
72
+ The design evolved through several reviews and SDK snapshots. For historical
73
+ assumptions, see the archived review disposition. Current contracts live in declarations
74
+ and recipes; plugin authors do not need the old review sequence to use
75
+ this package. Exact target SDK verification is an explicit generated job, separate
76
+ from ordinary installed-peer checks.
77
+
78
+ ## Current implementation direction
79
+
80
+ Core supplies attribution and invocation lifecycle context separately from
81
+ package composition, including a persisted storage-scoped
82
+ `experimental_p6rIdentity.instanceId`. Identity forgery resistance is explicitly
83
+ not a supported guarantee in this trusted deployment. The package rejects mismatched instance facts
84
+ and owns target/state validation; features still own atomic storage, schemas,
85
+ conflict decisions and legacy-owner mappings.
86
+
87
+ The internal [portable server binding](bb-binding-runtime.ts) composes normalized
88
+ invocations, endpoint/state registration and cleanup. The headless
89
+ [client-state binding](client-state-binding-runtime.ts) owns controller/view
90
+ lifetime and draft recovery while borrowing its client and connection. Both are
91
+ exposed through the bounded public entries below. Their isolated packed-artifact
92
+ and mounted-browser checks pass; actual consumer adoption and identical-plugin
93
+ base/fork proof remain separate gates.
94
+
95
+ The internal [RPC foundation](bb-runtime.ts) preserves native bound handlers and
96
+ owns their exact registration cleanup. The internal [state RPC bridge](state-rpc-runtime.ts)
97
+ uses it to dispatch registered resources, validate actor/session/address, and
98
+ revalidate request plus resource lifetime inside the feature transaction.
99
+ These are implementation components of the eventual bind-once surface, not new
100
+ public requirements for feature authors. HTTP paths map explicitly to legal
101
+ dotted native RPC methods; see [the wire contract](CONSUMERS.md).
102
+
103
+ Keep the first real-consumer goals visible: Thread Progress for personal state
104
+ and view-as, Agent Connect for external contribution acceptance, then
105
+ Notifications as a third consumer. Offline fixtures establish specific safety
106
+ properties but do not replace those migrations or the master plan's
107
+ upstream → fork → upstream → fork authored-session compatibility proof.
108
+
109
+ ## Entry points
110
+
111
+ | Import suffix | Runtime/declaration status | Responsibility |
112
+ | --- | --- | --- |
113
+ | root | Runtime: [index-runtime.ts](index-runtime.ts); declarations: [index.d.ts](index.d.ts), [model.d.ts](model.d.ts) | Portable keys, profiles, errors, codecs, provenance, receipt evidence and DTOs |
114
+ | `/bb` | Packaged runtime: [bb-entry-runtime.ts](bb-entry-runtime.ts); narrow declarations: [bb.d.ts](bb.d.ts) | `bindBbIdentity(bb)` returning `Result`; native RPC/state/server/endpoint/provider/tool/background composition and HTTP response lifetimes. |
115
+ | `/host` | Runtime: [host-runtime.ts](host-runtime.ts); declarations: [host.d.ts](host.d.ts) | Structural optional host protocol and package-owned normalization; integration/test ports |
116
+ | `/server` | Runtime: [server-runtime.ts](server-runtime.ts); declarations: [server.d.ts](server.d.ts) | Request actions, read/write targets, live commit validator and client endpoint |
117
+ | `/state` | Runtime: [state-runtime.ts](state-runtime.ts); declarations: [state.d.ts](state.d.ts) | Feature storage, immutable operations, drafts, conflicts and synchronization; packaged core/service/transport/controller with isolated-consumer proof |
118
+ | `/client` | Packaged browser runtime: [client-entry-runtime.ts](client-entry-runtime.ts); declarations: [client.d.ts](client.d.ts) | Headless one-root connection, freshness, views, directory search and state/view binding; no React or BB SDK import |
119
+ | `/react` | Packaged browser runtime: [react-entry-runtime.ts](react-entry-runtime.ts); declarations: [react.d.ts](react.d.ts) | Provider/Context, native connection/client hooks, state/view/recovery hooks, shared avatar/label/picker/status controls |
120
+ | `/testing` | Runtime: [testing-runtime.ts](testing-runtime.ts); declarations: [testing.d.ts](testing.d.ts) | Layer harnesses (adapter, connection, state storage, clock), the `createIdentityWireFake` app/client fake and the `defineStateStorageConformance` suite; see [Testing kit](#testing-kit) |
121
+
122
+ External sends have an explicit rendering ownership choice at binding
123
+ construction. `bindBbIdentity(bb)` defaults to `externalMessageRendering: "host"`:
124
+ the binding adds one escaped sender envelope around the supplied prompt before
125
+ portable or enhanced acceptance, while the external author subject remains
126
+ structured provenance. A producer that already owns its sender envelope and
127
+ `<attached>` context opts in with:
128
+
129
+ ```ts
130
+ const identity = bindBbIdentity(bb, { externalMessageRendering: "producer" });
131
+ ```
132
+
133
+ Producer mode preserves the supplied prompt and still registers the external
134
+ source for provenance. This is a construction-time option, not per-send wire or
135
+ author metadata, and no mode is inferred by inspecting prompt text. Retries of
136
+ operation IDs reserved before a rendering upgrade retain the host's existing
137
+ idempotency/uncertainty contract; this binding has no durable cross-version
138
+ marker to identify the old rendering.
139
+
140
+ On an enhanced host, producer mode also requires the optional
141
+ `experimental_useProducerMessageRendering(): void` identity-protocol capability.
142
+ The binding enables that plugin-generation-scoped SDK rendering policy once at
143
+ construction and returns a compatibility error when an older enhanced host does
144
+ not provide it. Capability-absent portable upstream does not need this hook.
145
+
146
+ The package's public SDK binding imports `@get-bb/plugin-sdk`; generic layers
147
+ import neither that SDK nor React. Feature authors supply `bb`, not an
148
+ `UpstreamDriver`. Low-level ports remain for the binding implementation and tests.
149
+ Core implements a structural protocol without importing this package or its
150
+ private brands. Provider-directory callbacks return issuer/subject records;
151
+ the package normalizes references and composes directory behavior.
152
+
153
+ ## Testing kit
154
+
155
+ `/testing` is source conformance for consumers' own tests, not native host
156
+ proof. Two additions let a plugin prove its integration from its own tree:
157
+
158
+ - `createIdentityWireFake({ host, mode, resource, policy? })` is a wire-level
159
+ identity server for client and app tests. Pass a fresh SDK fake host
160
+ (`createFakePluginHost` from `@get-bb/plugin-sdk/testing`). The fake runs the
161
+ production `bindBbIdentity` binding, endpoint and state bridge on it, with
162
+ state in `createStateStorageHarness`, so bootstrap, directory and
163
+ `bb-identity.v1.state.*` answers come from the real server code under host RPC
164
+ semantics. `rpc` is a handler map for SDK `testing/app`
165
+ `renderSlot(…, { rpc })`; forward `onRealtime` publications to the slot's
166
+ `emitRealtime`. `mode: 'single-user'` uses the upstream singleton path;
167
+ `'multi-user'` adds `switchActor`, `addPerson` and `personKey`. Controls:
168
+ `failNextLoads`, `loseNextSaveResponse`, `holdLoads`, `holdSaves`,
169
+ `invalidateExternally`; observations: `calls`, `saveInputs`, `committed`.
170
+ The SDK app-hook host itself is the SDK's job (`testing/app`), not this package's.
171
+ - `defineStateStorageConformance({ test, open, address, values })` registers the
172
+ `AtomicStateStorage` contract checks against a feature's real storage: stable
173
+ empty versions, one initialize winner, unchanged and conflict outcomes,
174
+ immutable address-scoped operation ids with exact replay, expiry tombstones,
175
+ commit-time rejection that leaves no record or outcome (inside the
176
+ transaction when `insideCommit` is supplied), and restart durability through
177
+ `reopen`. A check you cannot run is recorded with `skip: { restart: reason }`;
178
+ a skip is a visible gap, never a pass.
179
+
180
+ ```ts
181
+ import { test } from 'bun:test';
182
+ import { Database } from 'bun:sqlite';
183
+ import { defineStateStorageConformance } from '@phosphorco/bb-identity/testing';
184
+ import { createFeatureStorage, featureAddress } from '../storage.ts';
185
+
186
+ defineStateStorageConformance({
187
+ test, values: [{ theme: 'dark' }, { theme: 'light' }], address: featureAddress,
188
+ open: ({ now }) => {
189
+ const db = new Database(':memory:');
190
+ return { storage: createFeatureStorage({ db, now }), insideCommit: () => db.inTransaction, close: () => db.close() };
191
+ },
192
+ skip: { restart: 'in-memory database' },
193
+ });
194
+ ```
195
+
196
+ ## Bring your own identity provider
197
+
198
+ The public authoring entry is `bindBbIdentity(bb)`, with one explicit
199
+ factory-time Result unwrap before `binding.registerProvider(provider)`.
200
+ `IdentityProvider`, `ProviderEvidenceV1`, `ProviderDirectoryRecord`, and
201
+ `ProviderRegistrationV1` are exported as types from `/bb`; their declarations
202
+ live in [host.d.ts](host.d.ts). This is a pluggable contract, not a fixed
203
+ dependency on the existing Identity Boundaries plugin.
204
+
205
+ | Author supplies | Package/host supplies | Required behavior |
206
+ | --- | --- | --- |
207
+ | Declared issuer IDs and plugin-owned configuration | Registration in the selected boundary's plugin lifecycle | Registration can run before any person is resolved; no login/bootstrap cycle |
208
+ | `resolve(evidence)` returning issuer, stable subject and presentation, or an explicit non-success result | Trusted ingress facts, cancellation and a bounded deadline | Preserve not-applicable, rejected and unavailable; no false single-user fallback |
209
+ | Optional paged `directory` and bounded `lookup` callbacks returning issuer/subject records | Canonical key construction and normalized directory/profile access | Feature plugins do not learn the provider's RPC shape or namespace |
210
+ | Candidate-local readiness plus provider refresh/disposal integration | Generation-scoped registration and replacement semantics | Readiness cannot resolve a person; failed reload retains the active provider; stale disposal cannot remove a replacement |
211
+
212
+ The deployment selects one boundary plugin; that plugin may route among several
213
+ issuers. This draft does not introduce arbitrary priority competition between
214
+ several simultaneously active boundary plugins. A resolver-only provider is a
215
+ valid intended case: current request identity still works, while unavailable
216
+ directory features are reported explicitly. Search and lookup remain independently
217
+ optional; self presentation comes from the resolved session.
218
+
219
+ The existing Tailscale-backed Identity Boundaries implementation is one concrete
220
+ consumer. A second review scenario must use a different IdP—for example a plugin
221
+ verifying identity supplied by an OIDC-aware trusted gateway. Verification,
222
+ issuer configuration and credential management remain the provider's work.
223
+ The review must identify exactly how required evidence reaches `resolve`, which
224
+ facts BB establishes, and any missing registration/configuration contract. Draft 3 replaces generic attributes with a versioned evidence envelope,
225
+ operator-selected credential fields and host-established ingress facts.
226
+ [The IdP recipe](CONSUMERS.md#bring-your-own-idp) states lifecycle and evidence ownership.
227
+
228
+ On ordinary BB without the required identity boundary, provider registration
229
+ returns an explicit unsupported result. Feature plugins still run with the
230
+ default user. Building a new login UI, token issuer or authentication product is
231
+ not implicitly added by the BYO-IdP interface. If a provider needs additional
232
+ host evidence or lifecycle support, identify the smallest generic missing
233
+ operation rather than placing that provider's implementation in core.
234
+
235
+ ## Consumer feasibility walkthroughs for perspectives
236
+
237
+ Use only declared public operations for the package side of each walkthrough.
238
+ Small compile-only consumer sketches are useful; do not implement the package
239
+ or rewrite plugin source to make a walkthrough pass.
240
+
241
+ | Consumer task | Walkthrough must reach | Source to inspect |
242
+ | --- | --- | --- |
243
+ | A new plugin stores one person's preferences | Server bind → client bootstrap → UI context → ordinary read/write → same behavior for upstream's default user | [binding](bb.d.ts), [state](state.d.ts), [Thread Progress](../../plugins/thread-progress/server.ts) |
244
+ | View Alice's state while Cole is acting | View switch → target selection → explicit write policy → actor/session change with dirty edits → recoverable outcome | [client](client.d.ts), [React](react.d.ts), [Progress UI](../../plugins/thread-progress/components/progress-inbox.tsx) |
245
+ | Link a phone through another plugin | ntfy handler → request-bound Notifications call → correct owner/actor → completion or expiry on both hosts | [ntfy](../../plugins/ntfy/server.ts), [Notifications](../../plugins/notifications/server.ts), [server](server.d.ts) |
246
+ | Accept an external contribution | Credential check → external send → receipt/uncertainty → read normalized author data and correlate the accepted work | [Agent Connect send](../../plugins/agent-connect/server.ts), [reader](../../plugins/agent-connect/message-api.ts), [correlation](../../plugins/agent-connect/call-events.ts) |
247
+ | Bring a different IdP | Register without a resolved user → inspect evidence → resolve issuer/subject → canonical reference → optional directory → refresh/reload/dispose | [provider interface](host.d.ts), [current provider](../../plugins/identity-boundaries/lib/provider.ts) |
248
+ | Reuse across separately bundled plugins | Published imports → independent React roots → explicit shared data access without shared view state → unsubscribe/reload | [packaging](PACKAGING.md), [Agentation root](../../plugins/agentation/lib/toolbar.ts) |
249
+
250
+ For each task, distinguish ordinary plugin business/storage code from identity
251
+ infrastructure the package promises to supply. Report whether it is expressible
252
+ now, needs a declaration change, needs a generic enhanced-host operation, or
253
+ cannot meet the stated guarantee on the selected upstream baseline. Include an
254
+ author-comprehension verdict: can someone starting here discover and correctly
255
+ use the intended path without reading our conversation?
256
+
257
+ ## Consumer composition
258
+
259
+ Server factories call `bindBbIdentity(bb)` and handle its Result. The binding owns its adapter,
260
+ endpoint and request wrappers and registers cleanup through `bb.onDispose`.
261
+ Its `rpc.register` takes the normal public SDK contract plus handler descriptors
262
+ that declare interactive, background or external operation intent. That intent
263
+ cannot override stronger host-provided origin facts. Use `http.route(method, path, {origin, handle}, options)` for existing HTTP APIs.
264
+ Interactive routes use local host authentication; external routes authenticate
265
+ their feature credential before effects. The invocation remains live until the
266
+ response body completes or cancels, and plugin disposal aborts it.
267
+
268
+ The binding reserves versioned identity and state routes inside its own plugin
269
+ namespace. `useBbIdentityConnection()` consumes the public native scoped RPC,
270
+ realtime and connection-state hooks; `useBbIdentityClient()` owns a client over
271
+ that one connection. Neither uses `BbContext` to guess a route. Headless and
272
+ content-script roots instead call `createIdentityFetchConnection` with an explicit
273
+ endpoint, fetch and root signal, then create one client and any number of borrowed
274
+ state transports from that connection. The complete source and cleanup recipes
275
+ are in [CONSUMERS.md](CONSUMERS.md).
276
+
277
+ The package supplies `IdentityViewPicker`, `IdentityViewStatus`, `IdentityAvatar`
278
+ and `IdentityLabel`. The picker/status borrow the enclosing Context; avatar and
279
+ label render supplied normalized presentation without fetching. Features render
280
+ their product-specific conflict and retry UI from view snapshots/actions:
281
+
282
+ ```tsx
283
+ <BbIdentity.Provider client={identityClient}>
284
+ <BbIdentity.Context>
285
+ <IdentityViewPicker labels={{
286
+ trigger: 'View as', self: 'My settings', viewingFallback: 'Another person',
287
+ dialog: 'Choose a person', search: 'Search people', noResults: 'No people found',
288
+ searchUnavailable: 'People search is unavailable',
289
+ }} />
290
+ <IdentityViewStatus labels={{ viewing: 'Viewing another person', returnSelf: 'Return to my settings' }} />
291
+ {/* Feature-owned state, conflict and recovery controls. */}
292
+ </BbIdentity.Context>
293
+ </BbIdentity.Provider>
294
+ ```
295
+
296
+ A manually created client is owned by its caller; Provider borrows it. Keep the
297
+ Provider mounted when a native client is temporarily null: its optional fallback
298
+ replaces children while the Provider retains pending preservation and failure
299
+ ownership. Keep feature draft storage above replaceable Context children. Context
300
+ owns a view it creates, or borrows an explicitly injected view. Missing Provider
301
+ is a setup error, never single-user fallback. Separate plugins may share an
302
+ explicit compatible client transport while retaining independent views; no
303
+ cross-bundle React singleton is assumed.
304
+
305
+ `useIdentityStateBinding` returns `null` while committed acquisition is pending,
306
+ then connects that default Context to the headless
307
+ `bindIdentityState`: identity changes, guard registration, controller ownership,
308
+ reconnect and immediate detach live in the package. Features provide a shared state resource, storage and conflict policy; a
309
+ custom controller factory remains a low-level option. The packaged state HTTP
310
+ adapter handles repeated routing, decoding and publication. Recovery commands
311
+ remain reachable through the binding and reject stale controller tokens. View
312
+ guards participate in the same transition lifetime; `/bb.http.route` supplies
313
+ request-bound server HTTP handlers. Feature-specific conflict/retry UI remains
314
+ feature-owned. See the public declarations for required component props and
315
+ method arguments.
316
+
317
+ ## Core semantics
318
+
319
+ **Actor and target.** Cole viewing Alice remains Cole. Read target requests need
320
+ no write expectations. A write requires a write-intent target, actual actor and
321
+ server session stamp, plus the expected subject. Authored sends also require
322
+ actor/session expectations. They are stale-intent assertions, not credentials.
323
+ A plugin can explicitly allow collaborator edits while recording the actual
324
+ editor. Merely switching perspective never changes authorship.
325
+
326
+ **Freshness.** Server session DTOs include instance, actor, mode, capabilities
327
+ and a stamp. The stamp rotates on actor/provider discontinuity, including
328
+ A→B→A, while ordinary bootstrap refresh keeps the same continuity stamp.
329
+ Reconnect suspends writes until revalidation; it may reuse a stamp only if the
330
+ host proves continuity. Client load/transition generations fence stale results.
331
+ Owner-session IDs separately distinguish local controller incarnations.
332
+
333
+ **Default owner.** Without identity support, bindings use one reserved `local`
334
+ storage namespace and default identity, equal across plugin IDs and recreated
335
+ clients. This namespace is scoped by the backing host store, not a globally
336
+ unique person. Explicit configuration can select another stable namespace.
337
+ Hostname, browser ID, package instance and a random ID per plugin are not
338
+ sources of ownership. Merging several host stores or toggling identities needs
339
+ an explicit feature mapping. Malformed/unsupported extensions and removed or
340
+ unavailable providers never become capability absence.
341
+
342
+ **Provider lifecycle.** One selected boundary plugin may route its declared
343
+ issuers internally. `ProviderRegistrationV1` exposes an opaque generation,
344
+ its immutable non-secret configuration snapshot and normalized `person`; it is not a
345
+ request lease. Each invocation gets a fresh core-only active-provider lease with
346
+ its own signal. The host's structural/configuration readiness is mandatory;
347
+ optional provider-local readiness receives its staged generation, configuration, deadline and signal but
348
+ never request evidence or credentials. The enhanced host publishes the complete
349
+ registry/routing generation before any synchronous callback or abort observation,
350
+ then atomically swaps a ready candidate. It retains the predecessor on every
351
+ candidate or activation failure and generation-checks retirement. Registration
352
+ disposal removes only its still-current registration, never the plugin generation.
353
+ Resolver deadlines and late results discard person evidence. The target shared-host
354
+ policy uses carried or stable machine attribution so ordinary operations continue. Provider-owned signing-key refresh
355
+ does not blanket-invalidate sessions; it explicitly signals an authentication
356
+ discontinuity when verified facts warrant it. Directory search and lookup remain
357
+ independently optional and report unsupported when absent. These enhanced-host
358
+ guarantees must not silently reuse an operator or person namespace on provider
359
+ failure. The pending machine fallback is an explicit actor and storage subject,
360
+ not a claim that the enhanced capability is absent.
361
+
362
+ The enhanced raw host protocol returns `ForkProviderRegistration`, whose successful
363
+ `person` values are wire/plain-string shaped. The adapter validates and brands that
364
+ result before returning public `ProviderRegistrationV1` from `/bb`; provider authors
365
+ never receive `Wire`. This keeps core independently constructible without leaking a
366
+ package-private key brand across the raw boundary.
367
+
368
+ **Acceptance.** Upstream supports a check immediately before dispatch and native
369
+ submission evidence; the adapter cannot interlock with its internal transaction.
370
+ An upstream receipt therefore has no invented acceptance timestamp or contribution
371
+ IDs and retains whatever native delivery/queue/turn IDs were actually returned.
372
+ The fork checks the live handle in durable acceptance and can return structured
373
+ receipts. An operation is scoped to instance/plugin and bound to its immutable
374
+ payload, including the external author snapshot for external sends. Enhanced receipts have explicit retention. Reconciliation can read a
375
+ receipt after its request context is no longer live without resubmitting the operation.
376
+ Unknown/retained-outcome lookup never becomes proof that an earlier operation cannot
377
+ still commit.
378
+
379
+ **Nested calls.** `PersonRequest.callPlugin` forwards the live request through
380
+ an enhanced host-bound path. On upstream it uses the one-user convention with
381
+ ordinary SDK calls, without claiming transport-origin or transactional lifetime
382
+ guarantees unavailable there. Background wrappers establish explicit non-person
383
+ work. This distinction is declared, not hidden behind an actor-key payload.
384
+ The exact target SDK binding and unwrapped third-party call behavior remain
385
+ integration proofs; independent route credentials remain separate from attribution.
386
+
387
+ **Provenance.** Ordered attempt input groups refer to original contributions,
388
+ interaction resolutions and generated context. Known provenance contains every
389
+ referenced contribution snapshot as of that attempt; later edits cannot change
390
+ it. Partial/unknown provenance is explicit. Participants aggregate roles across
391
+ retained visible durable history, including external authors and mentioned-only
392
+ people. Queue-only drafts are not participants. Pages and previews report
393
+ coverage. Profile snapshots for mentions need no invented authorship evidence.
394
+
395
+ ## State and transition semantics
396
+
397
+ - The server captures target facts and lifecycle signal for `CommitValidator`.
398
+ `createStateService` receives that validator explicitly. It validates write
399
+ intent, actual actor/session, subject, instance/plugin/collection/record and
400
+ schema inside the synchronous local storage transaction, immediately before
401
+ mutation, with no await through commit. This boundary is same-process only;
402
+ a remote storage/host process needs a separately proven protocol.
403
+ - Deduplication precedes CAS. Same operation and payload replays the original
404
+ outcome; changed payload under that ID is invalid. Receipts and records commit
405
+ atomically. An old receipt cannot regress the client's newer acknowledged state.
406
+ - `OperationLookup` separates final, pending, absent-final and unknown/expired.
407
+ Even absent-final permits only the same immutable operation; it does not permit
408
+ replacing an uncertain operation with a new ID while the old one could arrive.
409
+ - Draft checkpoints hold original acknowledged base, latest desired value and
410
+ generation, and the entire exact in-flight mutation. Checkpoint before dispatch.
411
+ New local edits never replace that submitted payload. Draft keys include the
412
+ controller incarnation; conditional deletion cannot erase a newer checkpoint.
413
+ - Conflict policy handles every remote divergence while dirty, including loads,
414
+ reconnect and epoch changes. The original base, local desired value and remote
415
+ record are exposed with a conflict token scoped to the controller incarnation.
416
+ A later divergence supersedes that token even before the controller changes.
417
+ Acknowledgments apply only to their submitted generation. CAS alone cannot
418
+ compensate for implicitly rebasing an old document.
419
+ - Initialization has its own mutation kind. A competing initializer's record
420
+ wins; defaults never automatically rebase. Empty versions are stable; resetting
421
+ or recreating a record changes epoch. Unchanged results require matching schema.
422
+ - Mandatory identity invalidation immediately stops edits, timers and stale
423
+ dispatch. Cleanup is unconditional; draft persistence is fallible and separate.
424
+ Unpersisted intent reaches a recovery callback. Discard cannot recall accepted
425
+ writes. Flush retries are bounded. Optimistic edit success does not promise
426
+ crash durability: only completed checkpoints do, including before dispatch.
427
+ - Voluntary transitions first inspect all guards without destructive changes,
428
+ then prepare/flush/persist, then commit the selection synchronously after a
429
+ freshness check. A failed or superseded transition cannot partially discard
430
+ another editor's draft. Completed saves are not rolled back. Post-selection
431
+ draft cleanup is conditional and cannot delete newer intent.
432
+ - Default voluntary policy is block with an explanation. During outage, the
433
+ last-ready view may remain visibly stale/read-only while edits are suspended.
434
+ Controls provide loading, empty, error, retry and blocked feedback; focus stays
435
+ on failure and is restored when a successful reset removes the banner.
436
+ - Migrations remain feature-adapter obligations: scope mappings to the feature,
437
+ compare source/destination versions atomically, reject unresolved collisions,
438
+ and share mapping policy between import and runtime legacy reads. Unused
439
+ `MigrationPlan`/`InteractionProvenance`/conformance-report exports were removed.
440
+
441
+ ## Verification and remaining evidence
442
+
443
+ The archived review disposition records all seven panel results.
444
+ [Consumer paths](CONSUMERS.md) cover preferences, recovery, nested requests,
445
+ external reads/correlation, BYO IdP and independent roots through public declarations. The
446
+ [packaging contract](PACKAGING.md) specifies library generation and artifact
447
+ checks without editing generated manifests.
448
+
449
+ From the plugins repository:
450
+
451
+ ```sh
452
+ node_modules/.bin/tsc --noEmit --strict --skipLibCheck false \
453
+ --module nodenext --moduleResolution nodenext --target es2023 \
454
+ --lib es2023,dom --types react \
455
+ packages/bb-identity/*.d.ts packages/bb-identity/type-tests/*.test-d.ts
456
+ ```
457
+
458
+ Compile-only assertions check ordinary read ergonomics, read/write separation,
459
+ public SDK binding, raw structural handle construction and signal-free query
460
+ DTOs. Runtime tests cover extension absence/outage classification, raw-to-public
461
+ provider normalization and readiness, scoped/external acceptance, and final
462
+ lookup decoding. Later conformance must use a packed artifact with two
463
+ independently bundled UI consumers and real upstream→fork→upstream→fork
464
+ sessions. Preserve native content, provider references and upstream grouped-edit
465
+ restrictions in that proof.
package/bb.d.ts ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * First-consumer BB binding. This is deliberately the portable native-RPC and
3
+ * state and HTTP composition with request-scoped response lifetimes.
4
+ */
5
+ import type {
6
+ BbPluginApi,
7
+ PluginRpcContract,
8
+ StandardSchemaV1InferInput,
9
+ StandardSchemaV1InferOutput,
10
+ } from '@get-bb/plugin-sdk';
11
+ import type { Disposable, ExecutionProvenance, InstanceId, Result, Scheduler } from './model.js';
12
+ import type { IdentityProvider, ProviderRegistrationV1, ServerSession } from './host.js';
13
+ import type { IdentityEndpoint, IdentityServer, PersonRequest, TargetPolicy } from './server.js';
14
+ import type { AtomicStateStorage, StateResource } from './state.js';
15
+
16
+ /** IdP authors use this entry without importing adapter implementation ports. */
17
+ export type {
18
+ IdentityProvider,
19
+ ProviderEvidenceV1,
20
+ ProviderBoundaryConfigurationV1,
21
+ ProviderDirectoryRecord,
22
+ ProviderRegistrationV1,
23
+ } from './host.js';
24
+
25
+ export type BbPromptInput = Parameters<BbPluginApi['sdk']['threads']['send']>[0]['input'][number];
26
+ /** Controls ownership of the external sender envelope at binding construction. */
27
+ export type ExternalMessageRendering = 'host' | 'producer';
28
+
29
+ /** Public SDK members consumed by this binding; unrelated SDK features may evolve independently. */
30
+ export type BbIdentityApi = Pick<BbPluginApi, 'pluginId' | 'onDispose'> & {
31
+ readonly rpc: Pick<BbPluginApi['rpc'], 'register'>;
32
+ readonly http?: Pick<BbPluginApi['http'], 'route'>;
33
+ readonly realtime: Pick<BbPluginApi['realtime'], 'publish'>;
34
+ readonly sdk: {
35
+ /** Only the acknowledgement is common to supported 0.4 and 0.5 SDKs. */
36
+ readonly threads: {
37
+ readonly send: (args: Parameters<BbPluginApi['sdk']['threads']['send']>[0]) => Promise<{ readonly ok: true }>;
38
+ };
39
+ readonly plugins: Pick<BbPluginApi['sdk']['plugins'], 'callRpc'>;
40
+ };
41
+ };
42
+
43
+ /** Issued only while a registered native handler is executing. */
44
+ export interface BbInvocation {
45
+ readonly origin: 'interactive-user' | 'agent' | 'background' | 'external';
46
+ readonly signal: AbortSignal;
47
+ /** There is no ambient actor. The handler opens an explicit request; machine fallback is valid when no person is verified. */
48
+ person(): Promise<Result<PersonRequest<BbPromptInput>>>;
49
+ }
50
+
51
+ export type IdentityRpcHandlers<C extends PluginRpcContract> = {
52
+ readonly [M in keyof C]: {
53
+ /** Declares upstream operation intent; it cannot override host evidence. */
54
+ readonly origin: 'interactive-user' | 'background' | 'external';
55
+ readonly handle: (
56
+ input: StandardSchemaV1InferOutput<C[M]['input']>,
57
+ invocation: BbInvocation,
58
+ ) => StandardSchemaV1InferInput<C[M]['output']> | Promise<StandardSchemaV1InferInput<C[M]['output']>>;
59
+ };
60
+ };
61
+
62
+ export interface IdentityHttpHandler {
63
+ readonly origin: 'interactive-user' | 'external';
64
+ readonly handle: (
65
+ context: Parameters<Parameters<BbPluginApi['http']['route']>[2]>[0],
66
+ invocation: BbInvocation,
67
+ ) => Response | Promise<Response>;
68
+ }
69
+
70
+ /** Server methods accept only the invocation issued to the current handler. */
71
+ export interface BbInvocationServer {
72
+ readonly instanceId: InstanceId;
73
+ /** Revalidates an issued write target immediately at the feature's transaction boundary. */
74
+ readonly commits: IdentityServer<unknown, unknown, BbPromptInput>['commits'];
75
+ session(invocation: BbInvocation): Promise<ServerSession>;
76
+ selfProfile(invocation: BbInvocation): Promise<Result<import('./model.js').IdentityProfile>>;
77
+ personRequest(invocation: BbInvocation): Promise<Result<PersonRequest<BbPromptInput>>>;
78
+ readonly sendExternal: IdentityServer<unknown, unknown, BbPromptInput>['sendExternal'];
79
+ readonly lookupOperation: IdentityServer<unknown, unknown, BbPromptInput>['lookupOperation'];
80
+ /** Bounded, validated provenance reads; disposal fences new raw history calls. */
81
+ readonly history: IdentityServer<unknown, unknown, BbPromptInput>['history'];
82
+ }
83
+
84
+ /** Endpoint operations use the same issued native or HTTP invocation. */
85
+ export interface BbInvocationEndpoint extends Disposable {
86
+ bootstrap(invocation: BbInvocation): Promise<Result<ServerSession>>;
87
+ selfProfile(invocation: BbInvocation): Promise<Result<import('./model.js').IdentityProfile>>;
88
+ search(invocation: BbInvocation, input: import('./model.js').DirectoryQuery): ReturnType<IdentityEndpoint<unknown>['search']>;
89
+ profiles(invocation: BbInvocation, input: import('./model.js').ProfileQuery): ReturnType<IdentityEndpoint<unknown>['profiles']>;
90
+ participants(invocation: BbInvocation, input: import('./model.js').ParticipantQuery): ReturnType<IdentityEndpoint<unknown>['participants']>;
91
+ participantPreviews(invocation: BbInvocation, input: import('./model.js').ParticipantPreviewQuery): ReturnType<IdentityEndpoint<unknown>['participantPreviews']>;
92
+ }
93
+
94
+ /**
95
+ * One binding owns its adapter, server, endpoint, native registrations and
96
+ * state-route registrations. `dispose()` retires all of them together.
97
+ */
98
+ export interface BbIdentityBinding extends Disposable {
99
+ readonly server: BbInvocationServer;
100
+ readonly endpoint: BbInvocationEndpoint;
101
+ readonly rpc: {
102
+ register<C extends PluginRpcContract>(contract: C, handlers: IdentityRpcHandlers<C>): Result<void>;
103
+ };
104
+ readonly http: {
105
+ /** Interactive routes require local host authentication. Scope lasts through body completion or cancellation. */
106
+ route(method: string, path: string, handler: IdentityHttpHandler,
107
+ options?: Parameters<BbPluginApi['http']['route']>[3]): Result<void>;
108
+ };
109
+ readonly state: {
110
+ register<T>(options: {
111
+ readonly resource: StateResource<T>;
112
+ readonly storage: AtomicStateStorage<T>;
113
+ /** Write policy; also used for reads unless readPolicy is supplied. */
114
+ readonly policy: TargetPolicy;
115
+ readonly readPolicy?: TargetPolicy;
116
+ }): Result<Disposable>;
117
+ };
118
+ /** Provider setup is lifecycle-scoped and never resolves an ambient person. */
119
+ registerProvider(provider: IdentityProvider): Promise<Result<ProviderRegistrationV1>>;
120
+ /** Tool provenance is evidence-only; the SDK tool context remains feature-owned. */
121
+ toolProvenance(context: unknown): Promise<Result<ExecutionProvenance>>;
122
+ /** Non-person work receives a package-issued background invocation. */
123
+ background<T>(run: (invocation: BbInvocation) => Promise<T>): Promise<T>;
124
+ }
125
+
126
+ /**
127
+ * Creates the supported portable native-RPC/state/HTTP binding. Capability absence
128
+ * uses the stable upstream singleton; malformed or unavailable enhanced hosts
129
+ * return their failure and never fall back. Callers intentionally unwrap one
130
+ * Result during server-factory setup.
131
+ */
132
+ export declare function bindBbIdentity(bb: BbIdentityApi, options?: {
133
+ /** Reserved storage-scoped upstream namespace; enhanced hosts use their persisted raw instance fact. */
134
+ readonly stateNamespace?: string;
135
+ readonly scheduler?: Scheduler;
136
+ /** `host` (default) adds one sender envelope; `producer` preserves a producer-owned envelope. */
137
+ readonly externalMessageRendering?: ExternalMessageRendering;
138
+ }): Result<BbIdentityBinding>;