@prefig/upact 0.1.1

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/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # upact
2
+
3
+ A typed architectural contract between a social application and any identity provider. The application sees a small, capability-negotiated `Upactor`; the substrate (Supabase Auth, OIDC, peer-to-peer presence, etc.) stays behind the port.
4
+
5
+ ## Posture
6
+
7
+ upact is named for the Ulysses pact that adopters make when building privacy-first social technologies that are architecturally hostile to extractive practices.
8
+
9
+ upact is a minimum-disclosure anti-corruption layer between your application and any supported identity-management substrate. The privacy minima at the port (no email, no phone, no IP, opaque sessions, enumerated capabilities) are commitments the application can no longer violate. The architectural cost of breaking the contract later is what makes the commitment durable: an application built on upact cannot quietly pivot to surveillance-driven, data-retention-driven, or third-party-sharing-shaped revenue without tearing up its foundations.
10
+
11
+ The constraint also shapes design. When the application cannot know a user's email, you build features that don't need it. This provides friction against reflexes inherited from an extraction- and retention-shaped social media ecosystem. That friction matters especially with LLM-assisted development, where those patterns are efficiently automated by tools trained on that ecosystem.
12
+
13
+ upact is not a replacement for OIDC clients (`auth.js`, `lucia`, `openid-client`), identity-broker IDPs (Authentik, Keycloak, ZITADEL), or identity protocols (DIDs, Verifiable Credentials). It is the typed-contract layer that sits on top of these substrates and provides a contract on what the application is permitted to know and what it has bound itself out of knowing.
14
+
15
+ ## Why upact
16
+
17
+ The values commitment is what motivates platforms to consider adopting upact. These benefits accrue to every adopter who honours the contract, regardless of whether the values posture is what brought them in:
18
+
19
+ - **Design discipline.** Structural limits on what you can know force design principles and features that don't depend on data you shouldn't have.
20
+ - **A small, well-defined identity surface.** The `Upactor` type has three fields. Substrate-shaped User types typically have >=30.
21
+ - **Reduced surface for accidental privacy violations.** If application code can't read `email`, it can't accidentally include it in logs, metrics, error reports, SSR-rendered HTML, or analytics events. The architectural constraint doubles as a debugging-and-monitoring privacy guarantee.
22
+ - **Audited opacity primitives.** Sessions cannot be unwrapped via `JSON.stringify`, `structuredClone`, `util.inspect`, or other inspection vectors. The runtime kernel is centrally tested across sixteen vectors; every conforming adapter inherits the guarantee.
23
+ - **Swap substrates without re-architecting.** The port carries your privacy posture. Switch from Supabase Auth to an OIDC-brokered provider and your application code is unchanged.
24
+ - **A pre-written audit artefact.** When a third party (legal, compliance, regulator, partner) asks "what does your application know about users?", the conformance statement is the answer.
25
+
26
+ ## Usage
27
+
28
+ ```typescript
29
+ import type { IdentityPort } from '@prefig/upact';
30
+ import { SubstrateUnavailableError } from '@prefig/upact';
31
+
32
+ // authenticate returns Session | AuthError — discriminate on 'code':
33
+ const result = await port.authenticate({ kind: 'password', email, password });
34
+ if ('code' in result) {
35
+ switch (result.code) {
36
+ case 'credential_rejected': /* wrong password */ break;
37
+ case 'rate_limited': /* back off */ break;
38
+ default: /* substrate_unavailable, auth_failed */ break;
39
+ }
40
+ return;
41
+ }
42
+ // result is an opaque Session
43
+
44
+ // currentUpactor returns null when logged out; throws SubstrateUnavailableError when the substrate is unreachable:
45
+ try {
46
+ const actor = await port.currentUpactor(request);
47
+ if (!actor) { /* not logged in */ return; }
48
+ if (actor.capabilities.has('email')) { /* capability gated */ }
49
+ } catch (err) {
50
+ if (err instanceof SubstrateUnavailableError) { /* show maintenance page */ }
51
+ }
52
+ ```
53
+
54
+ `authenticate` communicates auth failures as return values (`AuthError`) — it never throws for wrong passwords or rate limits. `currentUpactor` and `issueRenewal` throw `SubstrateUnavailableError` for substrate outages — they never return error values. The asymmetry is deliberate: auth failures are expected control-flow; substrate outages are exceptional conditions.
55
+
56
+ ## Adapters
57
+
58
+ | Package | Substrate | Camp | Status |
59
+ |---|---|---|---|
60
+ | `@prefig/upact-supabase` | Supabase Auth | Enforcement | v0.1.0 shipped |
61
+ | `@prefig/upact-simplex` | SimpleX Chat daemon | Pre-conforming | v0.1.0 shipped |
62
+ | `@prefig/upact-oidc` | Any OIDC-compliant IDP (Dex, Authentik, Keycloak, ZITADEL) | Enforcement | v0.1.0 shipped |
63
+
64
+ ## Adopters
65
+
66
+ | Application | Substrate | Notes |
67
+ |---|---|---|
68
+ | [dyad.berlin](https://dyad.berlin) | `@prefig/upact-supabase` | First adopter; M1 integration in progress |
69
+
70
+ If your application uses upact, open a PR to add it here.
71
+
72
+ ## In this repo
73
+
74
+ - `SPEC.md` — normative specification, v0.1.
75
+ - `src/types.ts` — reference TypeScript types (`Upactor`, `IdentityPort`, `AuthError`, `Capability`, `Session`).
76
+ - `src/runtime.ts` — small runtime kernel. Exports `createSession`, the canonical factory that produces opaque `Session` values per SPEC.md §7.4. Adapter authors should use it rather than maintain their own opaque-wrapper class; the opacity guarantee is centralised here, audited once.
77
+ - `docs/adapter-shapes.md` — type-only sketches of the v0.1 shipped adapters (Supabase, SimpleX, OIDC).
78
+ - `docs/cross-adapter-findings.md` — cross-substrate observations that shaped the spec.
79
+ - `examples/sveltekit-supabase/` — minimal SvelteKit + Supabase integration showing the three key wiring points: hook, type augmentation, and capability-gated page load.
80
+ - `CONTRIBUTING.md` — the five-test contributor audit, AI-Involvement trailer convention.
81
+ - `GOVERNANCE.md` — v0.x maintainer posture and v1.0 working-group target.
82
+ - `CONFORMANCE.md` — conformance statement template with filled-in examples for both v0.1 adapters.
83
+ - `CHANGELOG.md` — per-version change record.
84
+ - `ROADMAP.md` — open and closed decisions.
85
+
86
+ ## Status
87
+
88
+ v0.1.1. Three reference adapters shipped. Breaking changes between v0.x revisions are permitted; v1.0 marks the first stable version.
89
+
90
+ ## Licence
91
+
92
+ Dual-licensed:
93
+
94
+ - **`SPEC.md`, `docs/`, this README, and other prose** — CC BY 4.0 (see `LICENSE`).
95
+ - **`src/` (TypeScript types and runtime kernel)** — Apache-2.0 (see `LICENSE-CODE`).
96
+
97
+ Each `src/*.ts` file carries an `SPDX-License-Identifier: Apache-2.0` header. The split follows TC39 / SPDX precedent: spec text is intellectual property meant to be cited and forked under CC; runtime code is software under a conventional permissive licence with a patent grant.
package/ROADMAP.md ADDED
@@ -0,0 +1,107 @@
1
+ # upact roadmap
2
+
3
+ Last updated: 2026-05-01 (rev. 7 — v0.1.1 shipped: lifecycle + provenance, Phase C OIDC adapter, Dex integration tests).
4
+
5
+ Open work for the v0.2 release window. Closed items are retained for institutional record.
6
+
7
+ ## Posture (load-bearing for everything below)
8
+
9
+ upact is a self-binding contract for values-aligned platform builders. The privacy minima at the port — no email, no phone, no IP, opaque sessions, enumerated capabilities — are commitments the application has structurally given up the ability to violate. The architectural cost of later breaking the contract is what makes the commitment durable.
10
+
11
+ upact is not a replacement for OIDC clients (`auth.js`, `lucia`, `openid-client`), identity-broker IDPs (Authentik, Keycloak, ZITADEL), or identity protocols (DIDs, Verifiable Credentials). It is the typed-contract layer above them.
12
+
13
+ **Adapter strategy (Path B).** Enforcement-camp substrates with OIDC-shaped auth (Supabase, Mastodon, Auth0, etc.) consume upact via `@prefig/upact-oidc`, which delegates substrate-specific machinery to a substrate-side IDP (Authentik, ZITADEL, etc.). Pre-conforming substrates (SimpleX, Reticulum) use direct adapters. This split is reflected in `docs/adapter-shapes.md`.
14
+
15
+ ## Open
16
+
17
+ ### Decision 3 — `issueRenewal` semantics normative
18
+
19
+ **Posture.** Supabase adapter renews the cookie holder (substrate-holder semantics); SimpleX adapter compares the freshly-read agentUserId against the prior `Upactor.id` and refuses renewal on mismatch (identity-bound semantics). Option A (identity-bound) is recommended per planning conversation — it is the more honest posture for a substrate where id stability is part of the contract.
20
+
21
+ **Status.** Deferred in `SPEC.md §12` (D3) until divergence between adapters becomes a concrete consumer issue. The §6.4 wording remains advisory.
22
+
23
+ ### Decision 6 — `provenance` field on `Upactor`
24
+
25
+ `provenance: { substrate: string; instance?: string }` for cross-substrate disambiguation. Shipped with the OIDC adapter (Phase C); concrete consumer: cross-IDP discrimination in multi-IDP deployments. Open question: whether Supabase and SimpleX adapters should also populate provenance for parity.
26
+
27
+ ### Decision 7 — `continuation` field on `Upactor`
28
+
29
+ `continuation: { prior_id: string; kind: 'rotation' | 'migration' | 'rekey' | 'reauth' }` for substrate-known identifier transitions. No shipped substrate currently emits transitions through the port. Reactivated when AP `Move`, SimpleX rotation, or similar arrives with a shipped adapter.
30
+
31
+ ### Decision 8 — `watch` on `IdentityPort`
32
+
33
+ `IdentityPort.watch(context): AsyncIterable<Upactor | null>`. Push-shaped substrates need an adapter first; the streaming-primitive choice (AsyncIterable vs Observable vs callback) deferred until a concrete push substrate ships.
34
+
35
+ ### v0.2 conformance test suite
36
+
37
+ **Posture.** A published conformance test suite (referenced in `SPEC.md §9` as "TBD") that adapter authors run to claim conformance mechanically. Currently only the sixteen-vector reflection test is standardised; the rest of the conformance bar is prose.
38
+
39
+ **Status.** Targeted for v0.2. Funding (OSA) would accelerate this.
40
+
41
+ ## Closed
42
+
43
+ ### v0.1.1 (2026-05-01)
44
+
45
+ **Shipped.** OIDC adapter (Phase C), lifecycle + provenance spec amendments, Dex integration test suite.
46
+
47
+ ---
48
+
49
+ ### Phase C: `@prefig/upact-oidc` adapter
50
+
51
+ **Closed 2026-05-01.** Third reference adapter shipped: enforcement-camp, OIDC-shaped. `createOidcAdapter(config, cookies)` factory — closure-captured cookie jar, no enumerable substrate state. PKCE (S256) + authorization-code flow, signed-cookie state and session management, transparent refresh on expiry, HMAC-SHA256 signed cookies. Scope policy enforces `email`/`phone`/`address`/`groups` exclusion at construction time. 75 unit tests + 11 Dex integration tests. Permanent Dex dev rig at `upact-oidc-poc/`.
52
+
53
+ ### Decision 9 (close) — `issueRenewal` normatively OPTIONAL
54
+
55
+ **Closed 2026-05-01.** See above; moved from Open.
56
+
57
+ ### `lifecycle` + `provenance` on `Upactor` (v0.1.1 spec amendment)
58
+
59
+ **Closed 2026-05-01.** Optional `lifecycle?: IdentityLifecycle` and `provenance?: { substrate: string; instance?: string }` added to `Upactor`. `IdentityLifecycle = { expires_at?: Date; renewable: 'reauth' | 'represence' | 'never' }`. Purely additive — existing adapters continue to work without populating these fields. The OIDC adapter populates both. Normative text in `SPEC.md §4.4` and `§4.5`.
60
+
61
+ ---
62
+
63
+ ### v0.1.0 (2026-05-01)
64
+
65
+ **Shipped.** First public draft of spec and runtime. All items below were completed as part of the v0.1.0 release window.
66
+
67
+ ---
68
+
69
+ ### `Upactor` rename across the codebase
70
+
71
+ **Closed 2026-05-01.** `UserIdentity` → `Upactor` throughout: `SPEC.md`, `src/types.ts`, `src/index.ts`, both adapter packages (`src/adapter.ts`, `src/identity-mapper.ts`, tests). Deprecated alias `UserIdentity = Upactor` retained for v0.1.x compatibility; removed at v0.2. Method `currentIdentity` → `currentUpactor` on the port.
72
+
73
+ ### Decision 4 — `AuthErrorCode` vocabulary normative in `§6.5`
74
+
75
+ **Closed 2026-05-01.** Six-member normative union: `credential_invalid`, `credential_rejected`, `substrate_unavailable`, `identity_unavailable`, `rate_limited`, `auth_failed`. Both reference adapters declare their mapping table in `CONFORMANCE.md`. Applications branch on `code` for substrate-portable error handling.
76
+
77
+ ### Decision 11 — Adapter package back-channel closure (conformance bar)
78
+
79
+ **Closed 2026-05-01.** Normative text added to `SPEC.md §7.5`: conforming adapters MUST hold substrate state in closure scope (factory pattern) or `#private` fields, not on enumerable instance properties. `(adapter as any).client` MUST return `undefined`. Both reference adapters (upact-supabase, upact-simplex) retrofitted to factory-only; `SupabaseUpactAdapter` and `SimpleXUpactAdapter` class forms removed. Both ship sixteen-vector back-channel reflection tests at `tests/back-channel.test.ts`.
80
+
81
+ ### `AI-Involvement` trailer convention
82
+
83
+ **Closed 2026-05-01.** Five-tier vocabulary documented in `CONTRIBUTING.md` and `ROADMAP.md`. Adopted across the prefig org for subsequent commits. Existing commits pre-convention carry no trailer; the convention start point is the v0.1.0 release.
84
+
85
+ ### v0.1 SPEC authorship policy (relaxed)
86
+
87
+ **Closed 2026-05-01.** The earlier commitment to a hand-rewritten normative spec before public push was relaxed for v0.1. `SPEC.md`, `src/types.ts`, and the runtime kernel ship with `AI-Involvement: authored` or `collaborative` trailer disclosure. The transparency posture (disclosure replaces authorship-purity as the binding mechanism) is consistent with the project's own values.
88
+
89
+ ### SPEC §6.2 amendment — `currentUpactor` throw permission
90
+
91
+ **Closed 2026-05-01.** Adapters MAY throw `SubstrateUnavailableError` (from `@prefig/upact`) when the substrate is unreachable, distinct from returning `null` when the user is not authenticated. Normative text in `SPEC.md §6.2`.
92
+
93
+ ### Decision 2 — `currentIdentity` throw-vs-null contract
94
+
95
+ **Closed 2026-05-01 (option C).** Typed `SubstrateUnavailableError` for substrate-down, `null` for logged-out. Both distinctions needed for quality UX without substrate coupling.
96
+
97
+ ### Decision 10 — Multi-step authentication flows at the port
98
+
99
+ **Closed 2026-05-01.** IDP delegation (Path B) resolves multi-step auth without growing the port. The OAuth dance happens at a substrate-side IDP; the upact adapter consumes terminal OIDC tokens. The port stays one-shot.
100
+
101
+ ### Decision 1 — `OpaqueSubstrateSession._unwrap()` escape hatch
102
+
103
+ **Closed 2026-05-01.** Lifted into `@prefig/upact` as `createSession` + `_unwrapSession`. Both reference adapters use `createSession`. Sixteen-vector opacity suite centrally maintained in `tests/runtime.test.ts`.
104
+
105
+ ### Decision 5 — Naming the central primitive
106
+
107
+ **Closed 2026-05-01.** `UserIdentity` → `Upactor`. Draws on the UML Actor lineage; coined word in the upact namespace; no external collisions; brand cohesion. Lower-case bare word — `Upactor`, not `UpActor`.
package/SECURITY.md ADDED
@@ -0,0 +1,33 @@
1
+ # Security
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Report security issues by email to **theodore@brave.berlin**. Do not open a public issue for a security vulnerability.
6
+
7
+ Include:
8
+
9
+ - A description of the vulnerability and its impact.
10
+ - Steps to reproduce.
11
+ - Any known mitigations or workarounds.
12
+
13
+ You will receive an acknowledgement within 72 hours and a resolution timeline within seven days.
14
+
15
+ ## What counts as a vulnerability in upact
16
+
17
+ upact's security surface is its port-level privacy guarantees. The following are in scope:
18
+
19
+ - **Session opacity bypass.** A technique that lets application code unwrap a `Session` and recover the substrate handle or user identifier via any reflection vector (`JSON.stringify`, `util.inspect`, `structuredClone`, `Proxy`, `WeakMap` side-channels, etc.).
20
+ - **Adapter back-channel leak.** A pattern that allows a conforming adapter to expose the substrate client (or substrate-internal tokens) through the adapter instance's enumerable properties or prototype chain, bypassing the Decision 11 (§7.5) closure-capture requirement.
21
+ - **`_unwrapSession` misuse vector.** A scenario where `_unwrapSession` from `@prefig/upact/internal` can be called by application code that is not supposed to have access to it, without importing from the `./internal` subpath explicitly.
22
+ - **Capability-check bypass.** A pattern where `capabilities` on an `Upactor` can be mutated by application code after construction.
23
+ - **`display_hint` email leakage.** An adapter pattern that allows email-shaped strings to reach `display_hint` in violation of §4.2 MUST NOT clauses.
24
+
25
+ The following are **out of scope**:
26
+
27
+ - Vulnerabilities in the underlying substrate (Supabase Auth, SimpleX, OIDC providers). Those are the substrate vendor's responsibility.
28
+ - Application-level misuse (e.g., an application that ignores upact and calls the substrate directly). upact does not prevent this; it is the architectural cost of explicit substrate coupling.
29
+ - Social-engineering attacks against adapter maintainers. These are supply-chain concerns and handled per the adapter repository's own security policy.
30
+
31
+ ## Conformance and security
32
+
33
+ A conforming adapter's security posture is described in its `CONFORMANCE.md`. Applications that need stronger threat-model guarantees (e.g. adversarial-context coordination) SHOULD choose a substrate whose threat model matches — see `SPEC.md §10`.
package/SPEC.md ADDED
@@ -0,0 +1,314 @@
1
+ # upact — Identity Port Specification
2
+
3
+ **Version:** 0.1
4
+ **Status:** Working draft. Public release. Breaking changes between v0.x revisions are permitted; v1.0 marks the first stable version.
5
+ **License:** CC BY 4.0
6
+
7
+ > **A note on authorship.** v0.1 of this specification was AI-co-authored under the project's `AI-Involvement` trailer convention (see `CONTRIBUTING.md`). The transparency posture is preserved as disclosure rather than authorship-purity. Every commit touching normative content carries the trailer; readers can audit the contribution lineage in `git log`. Future revisions may include a maintainer-only re-authoring pass; for now the disclosure trail is the binding mechanism.
8
+
9
+ ---
10
+
11
+ ## §1. Overview
12
+
13
+ This document specifies the *identity port* — a typed architectural contract between a social application and any identity provider. The port enforces minimum disclosure architecturally: even when the underlying provider exposes more, an application that conforms to this specification cannot consume what the port does not permit.
14
+
15
+ The port is intended to be small enough that any reasonable identity substrate — long-lived account-based providers (e.g. OAuth/OIDC), ephemeral presence-renewed providers, threshold-attested providers, peer-to-peer matching providers — can implement it with substrate-appropriate semantics, while the application layer remains substrate-agnostic.
16
+
17
+ This is the dual of selective-disclosure self-sovereign identity. SSI puts disclosure control at the *user* side: the application asks for what it wants, the user permits or denies. This specification puts a hard bound at the *application* side: the application architecturally cannot ask for what is outside the port, even if the user would permit it.
18
+
19
+ **The port is a self-binding contract.** The privacy minima at the port are not features the substrate happens to hide. They are commitments the application has structurally given up the ability to violate. The architectural cost of breaking the contract later is what makes the commitment durable: an application built on upact cannot quietly pivot to surveillance-driven, data-retention-driven, or third-party-sharing-shaped revenue without ripping out foundations.
20
+
21
+ ## §2. Terminology
22
+
23
+ - **Application** — software that consumes identities to serve users.
24
+ - **Provider** (also: *adapter*) — software that issues, attests, renews, and invalidates identities. Implements the `IdentityPort` interface (§6).
25
+ - **Substrate** — the implementation technology a provider is built on (database, OIDC server, IDP-brokered upstream, peer-to-peer messaging, etc.).
26
+ - **Upactor** — a `Upactor` value as defined in §4. The application's view of "who is this." (Renamed from `UserIdentity` in v0.1; deprecated alias `UserIdentity` remains for v0.1.x compatibility.)
27
+ - **Session** — provider-defined credential exchange artefact; opaque to the application.
28
+ - **Capability** — a self-described provider feature (§5).
29
+
30
+ The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted as in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119).
31
+
32
+ ## §3. Conformance
33
+
34
+ A **provider** (adapter package) conforms to this specification when it:
35
+
36
+ - exposes the four operations defined in §6,
37
+ - returns `Upactor` values matching §4,
38
+ - self-declares capabilities using the vocabulary in §5,
39
+ - honours every MUST NOT clause in §7,
40
+ - holds substrate state out of public reflection per §7.5.
41
+
42
+ An **application** conforms to this specification when it:
43
+
44
+ - consumes only `Upactor` values returned by a conforming provider,
45
+ - branches behaviour only on capability presence (§5), never on substrate identity, provider type, or out-of-band knowledge of the provider,
46
+ - does not reach past the port through reflection on adapter instances or sessions.
47
+
48
+ Conformance is to a specific version of the spec.
49
+
50
+ ## §4. Upactor
51
+
52
+ A `Upactor` is a value of the following shape:
53
+
54
+ ```ts
55
+ interface Upactor {
56
+ id: string;
57
+ display_hint?: string;
58
+ capabilities: ReadonlySet<Capability>;
59
+ lifecycle?: IdentityLifecycle;
60
+ provenance?: { substrate: string; instance?: string };
61
+ }
62
+
63
+ interface IdentityLifecycle {
64
+ expires_at?: Date;
65
+ renewable: 'reauth' | 'represence' | 'never';
66
+ }
67
+ ```
68
+
69
+ Five fields, three of which are optional. The shape is intentionally minimal per the contributor audit (CONTRIBUTING.md). `lifecycle` and `provenance` were deferred from the initial draft and brought back by the OIDC adapter (Phase C, v0.1.1) — concrete consumer need surfaces them. See §12 for the remaining deferred-decisions register.
70
+
71
+ ### §4.1 `id`
72
+
73
+ An opaque identifier, stable for the lifetime of this identity. The identity MAY be re-issued with a new `id` if the provider's substrate produces one (e.g. a presence-renewed substrate may issue a new `id` at each renewal); applications branch on equality only.
74
+
75
+ The `id` MUST be opaque to the application. Applications MUST NOT parse, decompose, or attribute meaning to the `id` beyond equality comparison.
76
+
77
+ ### §4.2 `display_hint`
78
+
79
+ An optional, best-effort string the application MAY render to other users when no petname is available. It MUST NOT be assumed unique. Applications SHOULD prefer petnames or local nicknames where the receiver has set them.
80
+
81
+ Display hints MUST NOT be email addresses, phone numbers, or other contact identifiers. They are display-only.
82
+
83
+ ### §4.3 `capabilities`
84
+
85
+ See §5.
86
+
87
+ ### §4.4 `lifecycle` (optional)
88
+
89
+ When the substrate has an explicit session TTL (e.g. OIDC JWT `exp` claim), providers SHOULD populate `lifecycle`. Substrates with no intrinsic expiry MAY omit this field.
90
+
91
+ `lifecycle.expires_at` is the absolute expiry time. `lifecycle.renewable` describes how the identity can be renewed: `'reauth'` (full credential exchange required), `'represence'` (presence renewal suffices), or `'never'` (identity does not expire).
92
+
93
+ Applications that want to render session-expiring-soon UI MAY use `lifecycle.expires_at`. Applications that do not care about lifecycle simply ignore this field.
94
+
95
+ ### §4.5 `provenance` (optional)
96
+
97
+ When present, `provenance.substrate` names the substrate type (e.g. `'oidc'`, `'supabase'`, `'simplex'`) and `provenance.instance` names the specific instance (e.g. the OIDC issuer URL). Used for cross-substrate discrimination in multi-IDP deployments.
98
+
99
+ Applications MUST NOT gate behaviour on `provenance.substrate` or `provenance.instance` as a substitute for capability checks. Provenance is informational, not authoritative.
100
+
101
+ ## §5. Capabilities
102
+
103
+ Capabilities are self-described provider features. The application uses capability presence to gate behaviour. The application MUST NOT branch on substrate identity, provider type, or any out-of-band knowledge of the provider.
104
+
105
+ ### §5.1 Core capability vocabulary (v0.1)
106
+
107
+ | Capability | Meaning |
108
+ |-----------|---------|
109
+ | `email` | The provider can deliver email to this identity. |
110
+ | `recovery` | The provider supports identity recovery flows. |
111
+
112
+ **The vocabulary is intentionally minimal.** The audit (see CONTRIBUTING.md) found these two are the capabilities that have shipped consumers — the Supabase reference adapter declares them; dyad's UI gating consumes them for password-reset and invitation flows. Capabilities present in earlier drafts (`messaging`, `p2p_matching`, `presence_renewal`, `threshold_attestation`, `push`, `webauthn`) were not declared by any shipped consumer in this version and were deferred. Adapter authors do not pre-emptively expand the vocabulary; new capabilities land via §5.2 extension when concrete consumers surface.
113
+
114
+ This minimum-viable discipline is itself part of the binding mechanism. A capability vocabulary that grew speculatively would dilute the contract: applications would gate on capabilities that no substrate genuinely supports, providers would declare capabilities to look feature-rich, and the whole signal would erode. Keeping the vocabulary small and concrete-need-driven keeps the binding genuine.
115
+
116
+ ### §5.2 Capability extension
117
+
118
+ Providers MAY self-declare capabilities outside the core vocabulary. Applications SHOULD treat unknown capabilities as absent.
119
+
120
+ A capability registry is maintained out-of-band. New capabilities move into a future revision of the core vocabulary on demonstrated implementation by at least two independent providers and demonstrated consumption by at least one application. Until those conditions are met, the capability remains adapter-local.
121
+
122
+ ### §5.3 Capability access
123
+
124
+ A capability does not, by itself, grant the application access to the substrate's underlying mechanism. To use a capability, the application invokes a separate, capability-bound operation (e.g. `EmailChannel.send(identity, message)`) defined per-capability and outside the scope of this document. The application MUST NOT bypass the capability boundary by reaching into the provider's substrate directly.
125
+
126
+ ## §6. Operations
127
+
128
+ A provider implements the following four operations:
129
+
130
+ ```ts
131
+ interface IdentityPort {
132
+ authenticate(credential: unknown): Promise<Session | AuthError>;
133
+ currentUpactor(request: Request): Promise<Upactor | null>;
134
+ invalidate(session: Session): Promise<void>;
135
+ issueRenewal(identity: Upactor, evidence: unknown): Promise<Upactor | null>;
136
+ }
137
+ ```
138
+
139
+ **Multi-step authentication flows are out of scope for the port.** When a substrate is OIDC-shaped (Authentik, Keycloak, ZITADEL, Mastodon-as-broker, GitHub-as-broker, etc.), the OAuth dance happens at a substrate-side IDP; the upact adapter consumes terminal OIDC tokens via the existing one-shot `authenticate` shape. Substrates with presentation-ready credentials (load-profile, single-call password) use `authenticate` directly. The port stays one-shot; flow-shaped complexity moves to a layer (IDP) better-equipped to handle it.
140
+
141
+ ### §6.1 `authenticate(credential)`
142
+
143
+ Establishes a session from a provider-shaped credential (password + email, OIDC tokenset, presence-event proof, threshold attestation, etc.). Returns a `Session` on success or an `AuthError` (§6.5) on failure. The shape of `credential` is provider-defined; the application either knows the shape because it is wired to that provider, or accepts the provider's UI to gather it.
144
+
145
+ The returned `Session` is opaque to the application; the application uses `currentUpactor` to obtain the `Upactor`.
146
+
147
+ ### §6.2 `currentUpactor(request)`
148
+
149
+ Returns the `Upactor` currently associated with a request, or `null` if no authenticated user.
150
+
151
+ The provider extracts the session credential from the request (cookie, header, captured tokenset, daemon state) according to its own conventions, validates it, and returns a fresh `Upactor` value.
152
+
153
+ A provider MAY throw a typed error (e.g. `SubstrateUnavailableError` exported by `@prefig/upact`) when the substrate is unreachable, to distinguish "no authenticated user" (logged-out) from "substrate cannot answer" (infrastructure outage). Applications that don't care let the error propagate; applications that want to render an outage-specific UI catch the type. The `null` channel is reserved for "logged-out."
154
+
155
+ ### §6.3 `invalidate(session)`
156
+
157
+ Invalidates the session. Subsequent calls to `currentUpactor` with a request bearing the same session MUST return `null`. The provider MAY cascade invalidation to associated identities.
158
+
159
+ ### §6.4 `issueRenewal(identity, evidence)`
160
+
161
+ Renews an existing identity, returning either a renewed identity (which MAY have a new `id`) or `null` if renewal is not possible or not supported. The shape of `evidence` is per-provider — for OIDC providers it is implicit (refresh token held in closure); for password-reauth substrates it is fresh credentials.
162
+
163
+ **`issueRenewal` is OPTIONAL** (Decision 9, closed normatively). Providers whose substrate has no concept of renewal (e.g. static profiles, never-expiring tokens) MUST return `null`. Consumers MUST treat a `null` response as "renewal not available" rather than as an error.
164
+
165
+ If the renewed identity has a new `id`, the application MUST treat data tied to the previous `id` according to the application's own data-model contract.
166
+
167
+ ### §6.5 `AuthError` vocabulary
168
+
169
+ The `AuthError` returned from `authenticate` carries a normative code drawn from a closed vocabulary:
170
+
171
+ ```ts
172
+ type AuthErrorCode =
173
+ | 'credential_invalid' // malformed credential rejected pre-substrate
174
+ | 'credential_rejected' // substrate rejected the credential
175
+ | 'substrate_unavailable' // substrate unreachable / network error
176
+ | 'identity_unavailable' // no such identity on this substrate
177
+ | 'rate_limited' // substrate rate-limited the operation
178
+ | 'auth_failed'; // unexpected substrate failure
179
+
180
+ interface AuthError {
181
+ code: AuthErrorCode;
182
+ message: string;
183
+ }
184
+ ```
185
+
186
+ Providers MUST return one of these codes; substrate-specific detail goes in `message`. Applications branch on `code` for substrate-portable error handling. Adapter packages document their per-substrate mapping (which substrate errors map to which code) in their conformance statement (§10).
187
+
188
+ ## §7. Privacy minima (normative MUST NOT clauses)
189
+
190
+ These clauses are normative. A conforming provider MUST observe them.
191
+
192
+ ### §7.1 No identifiers outside the contract
193
+
194
+ The `Upactor` value MUST NOT contain:
195
+
196
+ - email addresses
197
+ - phone numbers
198
+ - legal-name fields (`first_name`, `last_name`, etc.)
199
+ - date-of-birth fields
200
+ - IP addresses
201
+ - device identifiers
202
+ - any field of `app_metadata`, `user_metadata`, or substrate-specific extension blocks
203
+
204
+ If the substrate exposes such fields (Supabase Auth, OIDC, etc.), the provider's implementation of the port MUST strip them before returning a `Upactor` value.
205
+
206
+ ### §7.2 No silent enrichment
207
+
208
+ Providers MUST NOT add fields to `Upactor` that are not part of this specification. Future versions of this specification may add fields; providers conforming to a specific version MUST NOT include fields from later versions in values returned to applications conforming to the earlier version.
209
+
210
+ ### §7.3 No correlation handles
211
+
212
+ The `id` field MUST NOT be derivable from any user-supplied identifier (email, phone) by the application. Providers SHOULD use opaque random identifiers; if a provider derives `id` from a stable user attribute, the derivation MUST NOT be reversible by the application, and the provider MUST document the derivation in its conformance statement.
213
+
214
+ ### §7.4 No substrate exposure through Session — runtime kernel is normative
215
+
216
+ The `Session` value MUST be opaque to the application. Applications MUST NOT decompose, decode, or extract claims from a `Session` directly; the only valid use is to pass it back to the port (e.g. for `invalidate`). Substrate-shaped session structures (JWTs with claims, cookies with metadata, captured tokensets) are an implementation detail of the provider.
217
+
218
+ **Implementations MAY use `createSession` from `@prefig/upact`**, which provides the normative opacity guarantee tested across the sixteen reflection vectors in `tests/runtime.test.ts`. Implementations that do not use `createSession` MUST pass an equivalent vector suite to claim conformance: `JSON.stringify`, `Object.keys`, `Object.getOwnPropertyNames`, `Reflect.ownKeys`, `Object.getOwnPropertySymbols`, for-in iteration, `structuredClone`, `util.inspect`, direct property access, frozen-state immutability, and the controlled escape hatch via `_unwrapSession` from `@prefig/upact/internal`.
219
+
220
+ The runtime kernel is normative because the privacy minima at the type level are insufficient on their own — TypeScript's structural typing cannot prevent runtime reflection from leaking substrate state. The kernel turns the type-level guarantee into a runtime guarantee, centrally audited.
221
+
222
+ ### §7.5 Adapter back-channel closure
223
+
224
+ Conforming adapter packages MUST hold substrate state out of public reflection. Specifically:
225
+
226
+ - Substrate clients (e.g. `SupabaseClient`, `SimpleXClient`, OIDC token holders) MUST be held in closure-captured scope or ES2022 `#private` fields, never on enumerable instance properties. `(adapter as any).client` MUST be `undefined`.
227
+ - Adapter packages MUST restrict their `package.json` `exports` field to documented entry points only. Deep imports of internal modules MUST be unreachable through normal module resolution.
228
+ - Adapter packages MUST NOT export helpers that return substrate-typed values bypassing the port. Substrate-side operations live inside the adapter and are reached through documented helper paths (out-of-port) or not at all.
229
+ - Adapters MUST use upact's runtime primitives (`createSession`, `_unwrapSession`, `SubstrateUnavailableError`) rather than rolling alternative implementations.
230
+
231
+ The application's freedom to import substrate libraries directly is preserved — that is a transparent coupling, visible in `package.json` and reviewable in code. What §7.5 closes is the asymmetric case where an application uses upact's surface AND quietly cheats the contract through adapter-internal access paths. The conformance bar at §7.5 keeps the binding genuine.
232
+
233
+ Conformance verification: adapter packages SHOULD ship a sixteen-vector reflection test (mirroring the pattern in `@prefig/upact-supabase/tests/back-channel.test.ts`) asserting that no sentinel substrate token leaks via any common reflection path. Such tests are the operational form of §7.5.
234
+
235
+ ## §8. Identity lifecycle
236
+
237
+ Session lifecycle metadata surfaces through `lifecycle` on `Upactor` (§4.4). The field is optional; providers whose substrate has no intrinsic TTL omit it.
238
+
239
+ **The two patterns:**
240
+
241
+ - **Explicit TTL** (OIDC JWT `exp`, session-cookie `Max-Age`): providers populate `lifecycle.expires_at` with the absolute expiry time and set `lifecycle.renewable` to `'reauth'` (or `'represence'` for presence-renewal substrates). Providers SHOULD transparently refresh the session in `currentUpactor` when the token has expired and a refresh token is available; they SHOULD populate the updated expiry in the returned Upactor.
242
+ - **No intrinsic TTL** (Supabase server-side sessions, SimpleX profiles): providers omit `lifecycle`. The session remains valid until `invalidate` is called or the substrate revokes it.
243
+
244
+ Substrates with per-encounter rotation (a new `id` at each renewal) are covered by §6.4 and decision §12 D7; lifecycle modelling there waits for a shipped adapter.
245
+
246
+ ## §9. Provider conformance statement
247
+
248
+ A provider claiming conformance:
249
+
250
+ 1. SHALL ship a written conformance statement listing the version of this spec it conforms to and the capabilities it self-declares.
251
+ 2. SHALL document its substrate, its threat model, and any deviations from the spec's SHOULD-clauses (its MUST-clauses are not negotiable).
252
+ 3. SHALL document its `AuthError` mapping table (which substrate errors map to which §6.5 code).
253
+ 4. SHALL pass a sixteen-vector reflection test on the adapter instance (per §7.5) and document the test's existence in the conformance statement.
254
+ 5. SHALL pass the conformance test suite associated with its claimed version (test suite TBD; targeted for v0.2 or a funded follow-up).
255
+
256
+ A `CONFORMANCE.md` template ships in this repository with one filled-in example (the Supabase reference adapter).
257
+
258
+ ## §10. Security considerations
259
+
260
+ Privacy minima reduce the attack surface for credential theft, account takeover, and data correlation at the application layer. They do not prevent compromise of the substrate or of the provider's own secrets.
261
+
262
+ This specification does not prescribe a single threat model. Different providers serve different threat models through their substrate choices. Deployments SHOULD select a provider whose threat model fits their deployment context. Concretely:
263
+
264
+ - **Casual coordination** (e.g. neighbourhood gatherings, alpha tests, community events) is well-served by Supabase-backed or OIDC-brokered providers; the substrate's leakiness is acceptable in exchange for simplicity and ergonomics.
265
+ - **Anonymous / pseudonymous coordination** is well-served by pre-conforming substrates such as SimpleX (anonymous unidirectional queues, no central directory). The substrate's natural shape is already aligned with upact's privacy minima.
266
+ - **Adversarial-context coordination** (e.g. activist organising, source protection, mutual aid in adverse-power contexts) requires providers with stronger substrates: threshold-attested across non-colluding operators, peer-to-peer matching with no registry, mutual-vouching within a defined social graph. These are different *protocols*, not different parameters of the same provider; sketches deferred until shipped adapters surface.
267
+
268
+ The capability vocabulary itself is open-ended; an application that branches on a capability inherits whatever threat-model implications that capability carries (e.g. `email` implies the substrate handles email, which has known correlation properties).
269
+
270
+ ## §11. Versioning
271
+
272
+ This specification is versioned. v0.1 is the first public draft. Breaking changes between v0.x versions are permitted; v1.0 marks the first stable version.
273
+
274
+ Capabilities present in §5.1 are normative for v0.1. Capabilities outside §5.1 are advisory and follow registry conventions to be specified in v0.2. The registry MAY accept new capabilities on demonstrated implementation by at least two independent providers AND demonstrated consumption by at least one application.
275
+
276
+ Governance posture: v0.x decisions are made by the maintainer. By v1.0, decisions about the core capability vocabulary (§5.1) and MUST clauses (§7) move to a working group of ≥3 conforming-adapter authors. See `GOVERNANCE.md`.
277
+
278
+ ## §12. Deferred decisions (the register)
279
+
280
+ The audit (CONTRIBUTING.md) trimmed v0.1 to the minimum-viable surface that shipped consumers concretely need. Items below are deferred — not abandoned, just held until a concrete consumer surfaces or a shipped adapter forces the question.
281
+
282
+ | Decision | Substance | Why deferred from v0.1 |
283
+ |---|---|---|
284
+ | **D3 — `issueRenewal` semantics normative** | Pick identity-bound vs substrate-holder semantics as normative in §6.4 | Recommended Option A (identity-bound) per planning conversation; formal normative wording deferred until divergence between adapters becomes a consumer issue |
285
+ | **D7 — `continuation` field on Upactor** | Substrate-known transitions between identifiers (rotation, migration, rekey, reauth) | No shipped substrate currently emits transitions through the port. Reactivated when a substrate that does (Mastodon `Move`, Convex reactive rotation, etc.) ships an adapter. |
286
+ | **D8 — `watch` capability on the port** | `watch(context): AsyncIterable<Upactor \| null>` for substrate-side push events | Same as D7. Push-shaped substrates need an adapter first. |
287
+ | ~~**D9 — `issueRenewal` OPTIONAL in §6.4**~~ | **Closed v0.1.1.** Normative text added: providers MUST return `null` when renewal is unsupported. | Concrete need surfaced by OIDC adapter (some IDPs issue no refresh token). |
288
+ | ~~**D6 — `provenance` field on Upactor**~~ | **Closed v0.1.1.** `provenance: { substrate, instance? }` added to §4.5. | OIDC adapter needs cross-IDP discrimination at the port level. |
289
+ | ~~**F3 — Network-legible vs port-opaque identifier**~~ | **Closed v0.1.1.** The OIDC adapter holds `iss + sub` in closure for substrate-side calls; the port `id` is a hash-derived opaque string. The pattern is documented in §7.3 and the adapter's conformance statement. | Phase C adapter ships and names the pattern. |
290
+ | ~~**F6 — Lifecycle modelling has multiple shapes**~~ | **Closed v0.1.1.** Two patterns documented in §8: explicit-TTL and no-intrinsic-TTL. Per-encounter-rotation deferred (D7). | `lifecycle` field surfaces two real patterns shipped by OIDC adapter. |
291
+ | ~~**G1 — OIDC scope discipline**~~ | **Closed v0.1.1.** `@prefig/upact-oidc` ships `validateScopes` runtime guard and documents the scope allow-list in its conformance statement. | Phase C adapter ships with runtime enforcement. |
292
+ | **Convene + Reticulum substrate sketches in `docs/adapter-shapes.md`** | Speculative entries removed | No shipped adapter, no concrete consumer. Sketches return alongside their adapter. |
293
+
294
+ The audit checklist in CONTRIBUTING.md names the five tests every Decision passes (concrete-need / minimum-viable / substrate-agnosticism / binding-integrity / disclosure). Future contributors apply the same checklist; this register records the exit conditions for each deferred Decision.
295
+
296
+ ## §13. Non-normative appendix — provider sketches
297
+
298
+ Brief sketches of providers shipped or deferred against this port.
299
+
300
+ **Shipped at v0.1:**
301
+
302
+ - **`@prefig/upact-supabase`** — Supabase Auth substrate. Capabilities: `email`, `recovery` for users with email; `recovery` only for those without. Identity stable for the account lifetime; renewable via password reset. The port hides email, password, magic-links, JWT claims, `app_metadata`, `user_metadata` from the application; `capabilities.has('email')` gates email-bound features.
303
+
304
+ - **`@prefig/upact-simplex`** — SimpleX Chat substrate (anonymous unidirectional queues, no central directory). Capabilities: `[]` (no `email`, no `recovery`; substrate affordances for messaging and p2p-matching are real but documented in the adapter README rather than declared as capabilities — see §5 / CONTRIBUTING.md audit). Identity stable per loaded profile; renewable by re-loading the profile. No email, no recovery, no central user database.
305
+
306
+ **Shipped at v0.1.1:**
307
+
308
+ - **`@prefig/upact-oidc`** — generic OIDC client adapter delegating substrate-specific machinery to a substrate-side IDP (Authentik, Keycloak, ZITADEL, Dex). One adapter, many configured IDPs; substrate-specific machinery in IDP realm config rather than in adapter code. Adding a substrate (Mastodon-via-broker, GitHub-via-broker, etc.) becomes operational rather than code-level. Ships `lifecycle` (JWT `exp` → `expires_at`, `renewable: 'reauth'`), `provenance` (`substrate: 'oidc'`, `instance: issuer`), and runtime scope enforcement (`validateScopes`). 75 unit tests + 11 Dex integration tests.
309
+
310
+ The application code does not change across these. The deployment chooses the provider; the port carries the rest.
311
+
312
+ ---
313
+
314
+ *Document version: 0.1.1. Audit-trimmed and AI-co-authored under disclosure 2026-05-01. See `ROADMAP.md` for the full decision lineage and `CONTRIBUTING.md` for the audit discipline.*
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Typed errors that adapters MAY throw at port boundaries.
3
+ *
4
+ * SPEC.md §6.2 gives `currentUpactor` the return type
5
+ * `Promise<Upactor | null>`, where `null` means "no authenticated
6
+ * user." A substrate outage is a categorically different condition —
7
+ * the substrate cannot answer the question — and collapsing it into the
8
+ * `null` channel forces every caller to choose between misclassifying
9
+ * outage as logged-out or treating every `null` as suspicious.
10
+ *
11
+ * Adapters MAY throw `SubstrateUnavailableError` to keep the outage
12
+ * signal distinct from the logged-out signal. Applications that don't
13
+ * care let it propagate to their framework's error boundary (the same
14
+ * way any uncaught exception bubbles up). Applications that want to
15
+ * render an outage-specific banner catch this type.
16
+ *
17
+ * The `kind` field mirrors the `AuthError.code` vocabulary used at the
18
+ * `authenticate` boundary so the two error surfaces share one
19
+ * vocabulary across the port.
20
+ */
21
+ export declare class SubstrateUnavailableError extends Error {
22
+ readonly kind: "substrate_unavailable";
23
+ constructor(message?: string);
24
+ }
25
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;IACnD,QAAQ,CAAC,IAAI,EAAG,uBAAuB,CAAU;gBACrC,OAAO,SAAsC;CAIzD"}
package/dist/errors.js ADDED
@@ -0,0 +1,29 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Typed errors that adapters MAY throw at port boundaries.
4
+ *
5
+ * SPEC.md §6.2 gives `currentUpactor` the return type
6
+ * `Promise<Upactor | null>`, where `null` means "no authenticated
7
+ * user." A substrate outage is a categorically different condition —
8
+ * the substrate cannot answer the question — and collapsing it into the
9
+ * `null` channel forces every caller to choose between misclassifying
10
+ * outage as logged-out or treating every `null` as suspicious.
11
+ *
12
+ * Adapters MAY throw `SubstrateUnavailableError` to keep the outage
13
+ * signal distinct from the logged-out signal. Applications that don't
14
+ * care let it propagate to their framework's error boundary (the same
15
+ * way any uncaught exception bubbles up). Applications that want to
16
+ * render an outage-specific banner catch this type.
17
+ *
18
+ * The `kind` field mirrors the `AuthError.code` vocabulary used at the
19
+ * `authenticate` boundary so the two error surfaces share one
20
+ * vocabulary across the port.
21
+ */
22
+ export class SubstrateUnavailableError extends Error {
23
+ kind = 'substrate_unavailable';
24
+ constructor(message = 'identity substrate is unreachable') {
25
+ super(message);
26
+ this.name = 'SubstrateUnavailableError';
27
+ }
28
+ }
29
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IAC1C,IAAI,GAAG,uBAAgC,CAAC;IACjD,YAAY,OAAO,GAAG,mCAAmC;QACxD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;IACzC,CAAC;CACD"}