@agenticprimitives/connect-client 1.0.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Agentic Trust Labs
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,57 @@
1
+ # @agenticprimitives/connect-client
2
+
3
+ The **relying-party** half of Connect. A relying app makes no credential or OAuth request itself: it hands
4
+ off to the user's central-auth **Home** (the IdP) — which runs the credential ceremony (wallet / Google /
5
+ YouVersion / passkey) and custody — and gets back an SA-signed scoped delegation. Standard OIDC
6
+ (`authorize → code → /token`) with a seamless **FedCM-first** front.
7
+
8
+ See [spec 295](../../specs/295-relying-connect-client.md), [spec 230 / 259](../../specs/259-relying-idp-responsibility-split.md),
9
+ [ADR-0019](../../docs/architecture/decisions/0019-relying-site-authority-is-a-scoped-delegation.md),
10
+ [ADR-0021](../../docs/architecture/decisions/0021-generic-packages-vs-white-label-apps.md),
11
+ [ADR-0032](../../docs/architecture/decisions/0032-fedcm-delegation-via-substrate-grant-endpoint.md).
12
+
13
+ ## Why
14
+
15
+ YouVersion/Google OAuth clients are **origin-pinned to the registered Home origin**. If a relying app
16
+ originates the OAuth itself (from its own origin), strict providers reject it. The fix — and the doctrine —
17
+ is that the Home is the single credential/OAuth origin; relying apps only ever exchange a returned code. This
18
+ package is that relying-side protocol, extracted once instead of copied per app.
19
+
20
+ ## Usage
21
+
22
+ ```ts
23
+ import { createConnectClient, connectViaFedcm, connectViaPopup, connectViaRedirect } from '@agenticprimitives/connect-client';
24
+
25
+ // The app injects its domain.ts — NO hostnames live in this package (ADR-0021).
26
+ const connect = createConnectClient({
27
+ clientId: 'demo-web',
28
+ delegate: '0x…', // the relying-site backend account the Home scopes its grant to
29
+ redirectUri: () => window.location.origin + '/',
30
+ resolveAuthOrigin: (name) => resolveHomeOrigin(name), // app: name → <home> | apex
31
+ isAllowedIssuerOrigin: (o) => isAllowedHome(o), // app: issuer allowlist
32
+ });
33
+
34
+ // FedCM-first, then popup, then redirect (the seamless launcher):
35
+ try {
36
+ const r = await connectViaFedcm(await resolveHomeOrigin(''), 'demo-web', onProgress);
37
+ // r.idToken + r.delegation — done, no /token round-trip
38
+ } catch {
39
+ const p = await connectViaPopup(connect, name, 'demo-web-connect-relay', onProgress, signal);
40
+ if (p.status === 'blocked') {
41
+ const { url, stash } = await connectViaRedirect(connect, name);
42
+ sessionStorage.setItem('connect', JSON.stringify(stash));
43
+ window.location.href = url; // returns ?code&state
44
+ } else if (p.status === 'success') {
45
+ const tok = await connect.exchangeCode(p.authOrigin, p.code, p.codeVerifier);
46
+ await connect.verifyIdToken(p.authOrigin, tok.idToken, p.stash.nonce);
47
+ }
48
+ }
49
+ ```
50
+
51
+ On `?code&state` return (the redirect path), read the stash, `exchangeCode`, then `verifyIdToken`.
52
+
53
+ ## Boundary
54
+
55
+ Relying side only. The broker/Home side is `@agenticprimitives/connect`; credential ceremonies live at the
56
+ Home (`connect-auth`). This package imports only `@agenticprimitives/types` + `@agenticprimitives/fedcm-rp`,
57
+ is WebCrypto-only, and contains no hostnames.
@@ -0,0 +1,6 @@
1
+ export declare const PACKAGE_NAME = "@agenticprimitives/connect-client";
2
+ export declare const SPEC_REF = "specs/295-relying-connect-client.md";
3
+ export { createConnectClient, generatePkce, randomB64url, type ConnectClient, type ConnectClientConfig, type AuthorizeTemplate, type IdTokenClaims, type TokenResult, type EnrollmentStart, type WireDelegation, } from './oidc.js';
4
+ export { openCentralAuthPopup, preferRedirect, relayCodeIfPopupReturn, type PopupResult, } from './popup.js';
5
+ export { connectViaRedirect, connectViaPopup, connectViaFedcm, type ConnectStash, type PopupConnectResult, type FedcmConnectResult, } from './launch.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,YAAY,sCAAsC,CAAC;AAChE,eAAO,MAAM,QAAQ,wCAAwC,CAAC;AAE9D,OAAO,EACL,mBAAmB,EACnB,YAAY,EACZ,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,GACpB,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,sBAAsB,EACtB,KAAK,WAAW,GACjB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,eAAe,EACf,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ // @agenticprimitives/connect-client — the GENERIC relying-party connect client (spec 295).
2
+ //
3
+ // A relying app makes NO credential/OAuth request itself: it hands off to the user's central-auth Home
4
+ // (the IdP), which runs the credential ceremony (wallet / Google / YouVersion / passkey) + custody and
5
+ // returns a code; the relying app exchanges it for an SA-signed scoped delegation. Standard OIDC
6
+ // (authorize → code → /token) with a seamless FedCM-first front. NO hostnames here (ADR-0021) — the app
7
+ // injects its `domain.ts` via `ConnectClientConfig`. See specs/295-relying-connect-client.md.
8
+ export const PACKAGE_NAME = '@agenticprimitives/connect-client';
9
+ export const SPEC_REF = 'specs/295-relying-connect-client.md';
10
+ export { createConnectClient, generatePkce, randomB64url, } from './oidc.js';
11
+ export { openCentralAuthPopup, preferRedirect, relayCodeIfPopupReturn, } from './popup.js';
12
+ export { connectViaRedirect, connectViaPopup, connectViaFedcm, } from './launch.js';
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,EAAE;AACF,uGAAuG;AACvG,uGAAuG;AACvG,iGAAiG;AACjG,wGAAwG;AACxG,8FAA8F;AAE9F,MAAM,CAAC,MAAM,YAAY,GAAG,mCAAmC,CAAC;AAChE,MAAM,CAAC,MAAM,QAAQ,GAAG,qCAAqC,CAAC;AAE9D,OAAO,EACL,mBAAmB,EACnB,YAAY,EACZ,YAAY,GAQb,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,sBAAsB,GAEvB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,eAAe,GAIhB,MAAM,aAAa,CAAC"}
@@ -0,0 +1,50 @@
1
+ import type { ConnectClient, WireDelegation } from './oidc.js';
2
+ /** The PKCE/state stash the relying app must persist (sessionStorage) across a redirect, then hand to
3
+ * `exchangeCode`/`verifyIdToken` on `?code` return. */
4
+ export interface ConnectStash {
5
+ name: string;
6
+ state: string;
7
+ authOrigin: string;
8
+ codeVerifier: string;
9
+ nonce: string;
10
+ }
11
+ /** REDIRECT strategy: stash PKCE, then navigate to the Home. Returns the stash to persist BEFORE the
12
+ * navigation (the caller writes it to sessionStorage, then sets `window.location.href = start.url`). */
13
+ export declare function connectViaRedirect(client: ConnectClient, name?: string): Promise<{
14
+ url: string;
15
+ stash: ConnectStash;
16
+ }>;
17
+ export type PopupConnectResult = {
18
+ status: 'success';
19
+ authOrigin: string;
20
+ code: string;
21
+ codeVerifier: string;
22
+ stash: ConnectStash;
23
+ } | {
24
+ status: 'blocked';
25
+ } | {
26
+ status: 'cancelled';
27
+ } | {
28
+ status: 'error';
29
+ error: string;
30
+ };
31
+ /** POPUP strategy: open the Home's `/authorize` in a first-party popup; return the code IN PLACE (no
32
+ * navigation). On `blocked` the caller falls back to `connectViaRedirect` (ADR-0013, explicit not silent). */
33
+ export declare function connectViaPopup(client: ConnectClient, name: string | undefined, relayChannel: string, onProgress?: (msg: string) => void, signal?: AbortSignal): Promise<PopupConnectResult>;
34
+ export interface FedcmConnectResult {
35
+ status: 'fedcm-success';
36
+ authOrigin: string;
37
+ idToken: string;
38
+ delegation: WireDelegation;
39
+ }
40
+ /** FEDCM strategy (spec 264 / ADR-0032) — a TWO-step flow that keeps FedCM a thin identity adapter:
41
+ * 1. FedCM proves IDENTITY → a thin id_token (silent when the member's session is at the Home origin).
42
+ * 2. POST that id_token to the Home's `/fedcm/grant` → the scoped delegation. A server-custodied member
43
+ * (Google/KMS) is signed server-side (zero prompt); a device-custodied member returns
44
+ * `needs_device_credential` → THROWS → the caller falls back to popup/redirect (where the device signs).
45
+ * THROWS on dismissal / error / `needs_device_credential` / missing delegation — caller catches + falls back. */
46
+ export declare function connectViaFedcm(home: string, clientId: string, onProgress?: (msg: string) => void, opts?: {
47
+ configPath?: string;
48
+ grantPath?: string;
49
+ }): Promise<FedcmConnectResult>;
50
+ //# sourceMappingURL=launch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launch.d.ts","sourceRoot":"","sources":["../src/launch.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,aAAa,EAAmB,cAAc,EAAE,MAAM,WAAW,CAAC;AAIhF;wDACwD;AACxD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAMD;yGACyG;AACzG,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,YAAY,CAAA;CAAE,CAAC,CAG5H;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,YAAY,CAAA;CAAE,GAClG;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,GACvB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvC;+GAC+G;AAC/G,wBAAsB,eAAe,CACnC,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,YAAY,EAAE,MAAM,EACpB,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,EAClC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,kBAAkB,CAAC,CAgB7B;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,eAAe,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,cAAc,CAAC;CAC5B;AAED;;;;;kHAKkH;AAClH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,EAClC,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACjD,OAAO,CAAC,kBAAkB,CAAC,CAoB7B"}
package/dist/launch.js ADDED
@@ -0,0 +1,66 @@
1
+ // The seamless launcher (spec 295) — the three connect strategies, FedCM-first, in order:
2
+ // 1. FedCM (browser-native, silent when a Home session exists; spec 264 / ADR-0032),
3
+ // 2. first-party popup (WebAuthn/OAuth on the Home origin; COOP-relay return),
4
+ // 3. full-page redirect (the guaranteed fallback; popup-blocked / mobile).
5
+ // The relying app composes these (try FedCM → catch → popup → on blocked → redirect), so a device-custodied
6
+ // member who can't FedCM falls through to where they CAN sign. Generic (ADR-0021): the Home origin, the
7
+ // FedCM clientId, and the relay-channel name are all parameters.
8
+ import { fedcmGet } from '@agenticprimitives/fedcm-rp';
9
+ import { randomB64url } from './oidc.js';
10
+ import { openCentralAuthPopup } from './popup.js';
11
+ const stashOf = (name, s) => ({
12
+ name, state: s.state, authOrigin: s.authOrigin, codeVerifier: s.codeVerifier, nonce: s.nonce,
13
+ });
14
+ /** REDIRECT strategy: stash PKCE, then navigate to the Home. Returns the stash to persist BEFORE the
15
+ * navigation (the caller writes it to sessionStorage, then sets `window.location.href = start.url`). */
16
+ export async function connectViaRedirect(client, name) {
17
+ const start = await client.startEnrollment((name ?? '').trim());
18
+ return { url: start.url, stash: stashOf((name ?? '').trim(), start) };
19
+ }
20
+ /** POPUP strategy: open the Home's `/authorize` in a first-party popup; return the code IN PLACE (no
21
+ * navigation). On `blocked` the caller falls back to `connectViaRedirect` (ADR-0013, explicit not silent). */
22
+ export async function connectViaPopup(client, name, relayChannel, onProgress, signal) {
23
+ const trimmed = (name ?? '').trim();
24
+ // `openCentralAuthPopup` appends `mode=popup` itself — don't double-add it here.
25
+ const start = await client.startEnrollment(trimmed);
26
+ const stash = stashOf(trimmed, start);
27
+ // Accept the code from any origin the app trusts as a Home, not just the one we opened: a NAMELESS apex
28
+ // enroll is hopped by the Home to the member's `<label>` subdomain mid-ceremony (passkey rpId isolation /
29
+ // per-handle home), so the code is minted + returned from the subdomain. `res.authOrigin` is where it was
30
+ // actually minted → exchange THERE so the id_token `iss` matches (else exchange/verify fails on the hop).
31
+ const res = await openCentralAuthPopup(start.url, start.state, start.authOrigin, relayChannel, onProgress, signal, client.isAllowedIssuerOrigin);
32
+ if (res.status === 'success') {
33
+ return { status: 'success', authOrigin: res.authOrigin ?? start.authOrigin, code: res.code, codeVerifier: start.codeVerifier, stash };
34
+ }
35
+ return res;
36
+ }
37
+ /** FEDCM strategy (spec 264 / ADR-0032) — a TWO-step flow that keeps FedCM a thin identity adapter:
38
+ * 1. FedCM proves IDENTITY → a thin id_token (silent when the member's session is at the Home origin).
39
+ * 2. POST that id_token to the Home's `/fedcm/grant` → the scoped delegation. A server-custodied member
40
+ * (Google/KMS) is signed server-side (zero prompt); a device-custodied member returns
41
+ * `needs_device_credential` → THROWS → the caller falls back to popup/redirect (where the device signs).
42
+ * THROWS on dismissal / error / `needs_device_credential` / missing delegation — caller catches + falls back. */
43
+ export async function connectViaFedcm(home, clientId, onProgress, opts) {
44
+ const configPath = opts?.configPath ?? '/fedcm/config.json';
45
+ const grantPath = opts?.grantPath ?? '/fedcm/grant';
46
+ const { token: idToken } = await fedcmGet({
47
+ providers: [{ configURL: `${home}${configPath}`, clientId, params: { nonce: randomB64url(16), intent: 'signin' } }],
48
+ context: 'signin',
49
+ });
50
+ if (!idToken)
51
+ throw new Error('FedCM returned no id_token'); // → popup fallback
52
+ onProgress?.('Authorizing…');
53
+ const res = await fetch(`${home}${grantPath}`, {
54
+ method: 'POST',
55
+ credentials: 'include',
56
+ headers: { 'content-type': 'application/json' },
57
+ body: JSON.stringify({ id_token: idToken, client_id: clientId }),
58
+ });
59
+ const out = (await res.json().catch(() => ({})));
60
+ if (out.needs_device_credential)
61
+ throw new Error(`needs_device_credential:${out.via ?? ''}`); // → popup (device signs)
62
+ if (!res.ok || !out.delegation)
63
+ throw new Error(out.error ? `fedcm_grant_failed:${out.error}` : 'FedCM grant returned no delegation');
64
+ return { status: 'fedcm-success', authOrigin: home, idToken, delegation: out.delegation };
65
+ }
66
+ //# sourceMappingURL=launch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launch.js","sourceRoot":"","sources":["../src/launch.ts"],"names":[],"mappings":"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"}
package/dist/oidc.d.ts ADDED
@@ -0,0 +1,102 @@
1
+ import type { Address } from '@agenticprimitives/types';
2
+ /** A delegation as it rides on the wire (salt as a string). Opaque to this package — the relying app casts
3
+ * it to its own `DelegationWire`. We never interpret or verify it here (that is the substrate's job). */
4
+ export type WireDelegation = Record<string, unknown>;
5
+ /** The authorize template the Home runs. `site-login` = establish the connection; `org-create` = also
6
+ * deploy + custody an org SA. Apps may pass others the Home understands. */
7
+ export type AuthorizeTemplate = 'site-login' | 'org-create' | string;
8
+ /** App-injected configuration — the ONLY place hostnames/TLDs/origins live (in the app's `domain.ts`). */
9
+ export interface ConnectClientConfig {
10
+ /** The relying app's OIDC `client_id` (= its registered `aud`). */
11
+ clientId: string;
12
+ /** The relying-site delegate identity the Home scopes its grant TO (the backend account). */
13
+ delegate: Address;
14
+ /** The relying app's redirect URI (where the Home returns `?code&state`). Usually `origin + '/'`. */
15
+ redirectUri: () => string;
16
+ /** Resolve a name → the user's central-auth Home origin. Empty/unknown name ⇒ the platform apex.
17
+ * ONE mechanism, no fallback chain (ADR-0013). Supplied by the app's `domain.ts`. */
18
+ resolveAuthOrigin: (name?: string) => Promise<string> | string;
19
+ /** Hard allowlist check: is `origin` a well-formed, trusted Home issuer origin? (SEC-018) */
20
+ isAllowedIssuerOrigin: (origin: string) => boolean;
21
+ /** OIDC scope. Default `openid agent`. */
22
+ scope?: string;
23
+ }
24
+ export interface IdTokenClaims {
25
+ iss: string;
26
+ sub: string;
27
+ aud: string;
28
+ exp: number;
29
+ nonce?: string;
30
+ agent_name?: string;
31
+ canonical_agent_id?: string;
32
+ }
33
+ export interface TokenResult {
34
+ idToken: string;
35
+ /** The SA-signed scoped delegation the Home issued to the relying delegate (ADR-0041 — authority, not
36
+ * just identity). Opaque here; the app casts to its `DelegationWire`. */
37
+ delegation?: WireDelegation;
38
+ /** Present for `org-create`: the created org's identity + its broker delegation. */
39
+ org?: Record<string, unknown>;
40
+ }
41
+ /** The PKCE + state + nonce a started enrollment must round-trip back to `exchangeCode`/`verifyIdToken`. */
42
+ export interface EnrollmentStart {
43
+ ok: true;
44
+ url: string;
45
+ state: string;
46
+ authOrigin: string;
47
+ codeVerifier: string;
48
+ nonce: string;
49
+ }
50
+ /** A random base64url string of `n` bytes (state / nonce / PKCE verifier). */
51
+ export declare function randomB64url(n: number): string;
52
+ /** PKCE S256: a random verifier + `base64url(SHA-256(verifier))` challenge (RFC 7636). */
53
+ export declare function generatePkce(): Promise<{
54
+ verifier: string;
55
+ challenge: string;
56
+ }>;
57
+ interface AuthorizeParams {
58
+ authOrigin: string;
59
+ state: string;
60
+ nonce: string;
61
+ codeChallenge: string;
62
+ agentName: string;
63
+ template: AuthorizeTemplate;
64
+ orgBase?: string;
65
+ purpose?: string;
66
+ grantOrg?: Address;
67
+ /** Appended when launching in a popup (the Home posts the result back instead of redirecting). */
68
+ mode?: 'popup';
69
+ }
70
+ /** The relying-party OIDC client, bound to one app's config. */
71
+ export interface ConnectClient {
72
+ buildAuthorizeUrl(p: AuthorizeParams): string;
73
+ /** Build the `site-login` authorize request to the user's Home (PKCE + state + nonce). The app
74
+ * redirects to / popups `url`; the Home runs the ceremony and returns `?code&state`. */
75
+ startEnrollment(name: string, opts?: {
76
+ mode?: 'popup';
77
+ }): Promise<EnrollmentStart>;
78
+ /** Build an `org-create` authorize request (deploy + custody an org SA at the Home). */
79
+ startOrgCreation(personName: string, orgBase: string, opts?: {
80
+ purpose?: string;
81
+ grantOrg?: Address;
82
+ mode?: 'popup';
83
+ }): Promise<EnrollmentStart>;
84
+ /** Exchange the authorization `code` at the Home's `/token` (PKCE) → `{ idToken, delegation, org? }`. */
85
+ exchangeCode(authOrigin: string, code: string, codeVerifier: string): Promise<TokenResult>;
86
+ /** Verify the id_token against the Home's `/jwks` — ES256 alg-pinned, iss/aud/nonce/exp-bound, issuer
87
+ * allowlisted (SEC-018). The relying-site half of spec 230. */
88
+ verifyIdToken(authOrigin: string, idToken: string, expectedNonce: string): Promise<IdTokenClaims>;
89
+ /** Decode (no verification) the id_token payload. Use `verifyIdToken` for trust. */
90
+ decodeIdToken(idToken: string): IdTokenClaims;
91
+ /** The person SA address an id_token carries (`canonical_agent_id` / `sub`, CAIP-10 → Address). */
92
+ personAddressFromIdToken(idToken: string): Address;
93
+ /** Is `origin` a trusted Home issuer origin? (the app's `isAllowedIssuerOrigin`, surfaced so the popup
94
+ * launcher can accept the code from whichever trusted Home origin the ceremony COMPLETED on — the Home
95
+ * may hop a nameless apex enroll to the member's `<label>` subdomain mid-flow.) */
96
+ isAllowedIssuerOrigin(origin: string): boolean;
97
+ }
98
+ /** Construct the relying-party OIDC client for one app. All app-specific identity (client_id, delegate,
99
+ * redirect, the Home-origin resolver, the issuer allowlist) is injected — keeping this package generic. */
100
+ export declare function createConnectClient(config: ConnectClientConfig): ConnectClient;
101
+ export {};
102
+ //# sourceMappingURL=oidc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidc.d.ts","sourceRoot":"","sources":["../src/oidc.ts"],"names":[],"mappings":"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"}
package/dist/oidc.js ADDED
@@ -0,0 +1,129 @@
1
+ // The relying-party OIDC client (spec 295 / spec 230 / ADR-0019). A relying app does NOT run a broker and
2
+ // originates NO credential: it builds an `/authorize` request to the user's central auth Home, and the Home
3
+ // runs the credential ceremony (wallet / Google / YouVersion / passkey) + custody, then redirects back with
4
+ // `?code=…&state=…`. This module owns ONLY that relying-side protocol — PKCE, the authorize URL, the /token
5
+ // exchange, and ES256 id_token verification.
6
+ //
7
+ // GENERIC (ADR-0021): no hostnames, no `.impact` TLD, no host/subdomain parsing. The app injects a config
8
+ // object (`resolveAuthOrigin` / `isAllowedIssuerOrigin` / `clientId` / `delegate` / `redirectUri`) wiring its
9
+ // own `domain.ts`. The credential ceremonies live at the Home (connect-auth), never here.
10
+ // ── base64url + JWT segment helpers (WebCrypto only) ───────────────────
11
+ function b64url(bytes) {
12
+ let s = '';
13
+ for (const b of bytes)
14
+ s += String.fromCharCode(b);
15
+ return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
16
+ }
17
+ function fromB64url(seg) {
18
+ const bin = atob(seg.replace(/-/g, '+').replace(/_/g, '/'));
19
+ const out = new Uint8Array(bin.length);
20
+ for (let i = 0; i < bin.length; i++)
21
+ out[i] = bin.charCodeAt(i);
22
+ return out;
23
+ }
24
+ function decodeJwtSegment(seg) {
25
+ return JSON.parse(new TextDecoder().decode(fromB64url(seg)));
26
+ }
27
+ /** A random base64url string of `n` bytes (state / nonce / PKCE verifier). */
28
+ export function randomB64url(n) {
29
+ return b64url(crypto.getRandomValues(new Uint8Array(n)));
30
+ }
31
+ /** PKCE S256: a random verifier + `base64url(SHA-256(verifier))` challenge (RFC 7636). */
32
+ export async function generatePkce() {
33
+ const verifier = randomB64url(32);
34
+ const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
35
+ return { verifier, challenge: b64url(new Uint8Array(digest)) };
36
+ }
37
+ /** Construct the relying-party OIDC client for one app. All app-specific identity (client_id, delegate,
38
+ * redirect, the Home-origin resolver, the issuer allowlist) is injected — keeping this package generic. */
39
+ export function createConnectClient(config) {
40
+ const scope = config.scope ?? 'openid agent';
41
+ const buildAuthorizeUrl = (p) => {
42
+ const u = new URL('/', p.authOrigin);
43
+ u.searchParams.set('client_id', config.clientId);
44
+ u.searchParams.set('redirect_uri', config.redirectUri());
45
+ u.searchParams.set('response_type', 'code');
46
+ u.searchParams.set('scope', scope);
47
+ u.searchParams.set('state', p.state);
48
+ u.searchParams.set('nonce', p.nonce);
49
+ u.searchParams.set('code_challenge', p.codeChallenge);
50
+ u.searchParams.set('code_challenge_method', 'S256');
51
+ u.searchParams.set('agent_name', p.agentName);
52
+ u.searchParams.set('delegate', config.delegate);
53
+ u.searchParams.set('delegation_template', p.template);
54
+ if (p.orgBase)
55
+ u.searchParams.set('org_base', p.orgBase);
56
+ if (p.purpose)
57
+ u.searchParams.set('org_purpose', p.purpose);
58
+ if (p.grantOrg)
59
+ u.searchParams.set('grant_org', p.grantOrg);
60
+ if (p.mode)
61
+ u.searchParams.set('mode', p.mode);
62
+ return u.toString();
63
+ };
64
+ const begin = async (name, template, extra = {}) => {
65
+ const state = randomB64url(16);
66
+ const nonce = randomB64url(16);
67
+ const { verifier, challenge } = await generatePkce();
68
+ const authOrigin = await config.resolveAuthOrigin(name);
69
+ const url = buildAuthorizeUrl({ authOrigin, state, nonce, codeChallenge: challenge, agentName: name, template, ...extra });
70
+ return { ok: true, url, state, authOrigin, codeVerifier: verifier, nonce };
71
+ };
72
+ const decodeIdToken = (idToken) => decodeJwtSegment(idToken.split('.')[1] ?? '');
73
+ return {
74
+ buildAuthorizeUrl,
75
+ startEnrollment: (name, opts) => begin(name, 'site-login', { mode: opts?.mode }),
76
+ startOrgCreation: (personName, orgBase, opts) => begin(personName, 'org-create', { orgBase, purpose: opts?.purpose, grantOrg: opts?.grantOrg, mode: opts?.mode }),
77
+ async exchangeCode(authOrigin, code, codeVerifier) {
78
+ const r = await fetch(new URL('/token', authOrigin).toString(), {
79
+ method: 'POST',
80
+ headers: { 'content-type': 'application/json' },
81
+ body: JSON.stringify({ grant_type: 'authorization_code', code, code_verifier: codeVerifier, client_id: config.clientId, redirect_uri: config.redirectUri() }),
82
+ });
83
+ const b = (await r.json().catch(() => ({})));
84
+ if (!r.ok || !b.id_token)
85
+ throw new Error(b.error ?? `token exchange failed (HTTP ${r.status})`);
86
+ return { idToken: b.id_token, delegation: b.delegation, org: b.org };
87
+ },
88
+ async verifyIdToken(authOrigin, idToken, expectedNonce) {
89
+ if (!config.isAllowedIssuerOrigin(authOrigin)) {
90
+ throw new Error(`refusing to verify id_token: issuer "${authOrigin}" not in allowlist`);
91
+ }
92
+ const parts = idToken.split('.');
93
+ if (parts.length !== 3)
94
+ throw new Error('id_token malformed');
95
+ const [h, p, s] = parts;
96
+ const header = decodeJwtSegment(h);
97
+ const claims = decodeJwtSegment(p);
98
+ const { keys } = (await (await fetch(new URL('/jwks', authOrigin).toString())).json());
99
+ const jwk = keys.find((k) => k.kid === header.kid);
100
+ if (!jwk)
101
+ throw new Error('no JWKS key for id_token kid');
102
+ if (jwk.alg !== 'ES256' || header.alg !== 'ES256')
103
+ throw new Error('id_token alg not ES256');
104
+ const key = await crypto.subtle.importKey('jwk', jwk, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']);
105
+ const ok = await crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, fromB64url(s), new TextEncoder().encode(`${h}.${p}`));
106
+ if (!ok)
107
+ throw new Error('id_token signature invalid');
108
+ if (claims.iss !== authOrigin)
109
+ throw new Error('id_token iss mismatch');
110
+ if (!config.isAllowedIssuerOrigin(claims.iss))
111
+ throw new Error('id_token iss not in allowlist');
112
+ if (claims.aud !== config.clientId)
113
+ throw new Error('id_token aud mismatch');
114
+ if (expectedNonce && claims.nonce !== expectedNonce)
115
+ throw new Error('id_token nonce mismatch');
116
+ if (typeof claims.exp !== 'number' || claims.exp * 1000 <= Date.now())
117
+ throw new Error('id_token expired');
118
+ return claims;
119
+ },
120
+ decodeIdToken,
121
+ personAddressFromIdToken(idToken) {
122
+ const c = decodeIdToken(idToken);
123
+ const raw = c.canonical_agent_id ?? c.sub;
124
+ return (raw.includes(':') ? raw.split(':').pop() : raw);
125
+ },
126
+ isAllowedIssuerOrigin: config.isAllowedIssuerOrigin,
127
+ };
128
+ }
129
+ //# sourceMappingURL=oidc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidc.js","sourceRoot":"","sources":["../src/oidc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ export type PopupResult = {
2
+ status: 'success';
3
+ code: string; /** the Home origin the ceremony COMPLETED on (may differ from the
4
+ * opened origin after a nameless-apex → `<label>` subdomain hop) — use it for the code exchange. */
5
+ authOrigin?: string;
6
+ } | {
7
+ status: 'cancelled';
8
+ } | {
9
+ status: 'error';
10
+ error: string;
11
+ } | {
12
+ status: 'blocked';
13
+ };
14
+ /** Prefer a full redirect on mobile / narrow viewports (a popup opens as a tab there). */
15
+ export declare function preferRedirect(): boolean;
16
+ /** Open the Home's `/authorize` in a popup and resolve when it posts a result.
17
+ * @param redirectUrl the authorize URL the caller built (`mode=popup` is appended).
18
+ * @param expectedState the `state` the caller generated — the popup MUST echo it (replay guard, F5).
19
+ * @param expectedOrigin the RESOLVED Home origin the popup was OPENED at — always accepted (F3).
20
+ * @param relayChannel the app's BroadcastChannel name (the COOP relay fallback; must match the Home).
21
+ * @param isAllowedOrigin OPTIONAL: also accept a success message from any OTHER origin this predicate
22
+ * trusts as a Home. Needed because the Home may HOP a nameless apex enroll to the member's `<label>`
23
+ * subdomain mid-ceremony (passkey rpId isolation / per-handle home) — the code then arrives from the
24
+ * subdomain, not `expectedOrigin`. Still fail-closed: `e.source === popup` (the exact window we opened)
25
+ * + the echoed `state` (F5) + the single-use PKCE-bound code remain required; we only widen the ORIGIN
26
+ * set to the app's declared Home allowlist, never to "any origin". The result's `authOrigin` reports
27
+ * the origin the code actually came from so the caller exchanges it at the right `iss`.
28
+ * Resolves `blocked` if the popup couldn't open (caller falls back to a full-page redirect, ADR-0013). */
29
+ export declare function openCentralAuthPopup(redirectUrl: string, expectedState: string, expectedOrigin: string, relayChannel: string, onProgress?: (msg: string) => void, signal?: AbortSignal, isAllowedOrigin?: (origin: string) => boolean): Promise<PopupResult>;
30
+ /**
31
+ * The POPUP-SIDE half of the COOP relay — call this once at the relying app's entry (`main.tsx`), BEFORE
32
+ * rendering. When the OAuth IdP severed the popup's opener (COOP), the Home redirects this popup back to the
33
+ * relying origin with `?ac_relay=1&code&state` (+ `&ac_iss=<home origin>` when the Home that minted the code
34
+ * is not the origin the popup was opened at — e.g. an apex enroll that hopped to the member's `<label>`
35
+ * subdomain); we (the popup, now same-origin with the opener) hand `{code,state,iss}` to the opener over the
36
+ * same-origin BroadcastChannel `relayChannel` (which `openCentralAuthPopup` listens on) and close. Returns
37
+ * `true` when this load IS a relay return — the caller should then NOT render the app (just relay + close).
38
+ * Returns `false` for a normal load (render as usual). The relay-channel name must match the one passed to
39
+ * `openCentralAuthPopup`. Only `code`+`state`(+`iss` origin) cross the channel — never a token. */
40
+ export declare function relayCodeIfPopupReturn(relayChannel: string): boolean;
41
+ //# sourceMappingURL=popup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"popup.d.ts","sourceRoot":"","sources":["../src/popup.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,WAAW,GACnB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,CAAC;6GACoE;IACpG,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GACxB;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,GACvB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC;AAmB1B,0FAA0F;AAC1F,wBAAgB,cAAc,IAAI,OAAO,CAKxC;AAED;;;;;;;;;;;;2GAY2G;AAC3G,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,EACtB,YAAY,EAAE,MAAM,EACpB,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,EAClC,MAAM,CAAC,EAAE,WAAW,EACpB,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,GAC5C,OAAO,CAAC,WAAW,CAAC,CAoEtB;AAED;;;;;;;;;mGASmG;AACnG,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAapE"}
package/dist/popup.js ADDED
@@ -0,0 +1,161 @@
1
+ // Popup-based central-auth ceremony (spec 257 / spec 230). The relying app opens the Home's `/authorize`
2
+ // (with `mode=popup`) in a FIRST-PARTY popup — WebAuthn create/get + provider OAuth work there, on the Home
3
+ // origin, with no iframe storage partitioning — and receives a single OIDC authorization CODE back over a
4
+ // strictly origin-validated postMessage (+ a same-origin BroadcastChannel relay for when the OAuth IdP
5
+ // severs the opener under COOP). The relying app then exchanges the code at the Home's `/token`.
6
+ //
7
+ // Security (audit F3/F5): resolve ONLY on a message from the exact popup we opened, at the exact Home
8
+ // origin, whose echoed `state` matches. No tokens/delegations ever travel in the message or URL — only the
9
+ // code. Generic (ADR-0021): the Home origin + the relay-channel name are parameters, not constants.
10
+ /** Prefer a full redirect on mobile / narrow viewports (a popup opens as a tab there). */
11
+ export function preferRedirect() {
12
+ return ((typeof window.matchMedia === 'function' && window.matchMedia('(max-width: 640px)').matches) ||
13
+ /Mobi|Android/i.test(navigator.userAgent));
14
+ }
15
+ /** Open the Home's `/authorize` in a popup and resolve when it posts a result.
16
+ * @param redirectUrl the authorize URL the caller built (`mode=popup` is appended).
17
+ * @param expectedState the `state` the caller generated — the popup MUST echo it (replay guard, F5).
18
+ * @param expectedOrigin the RESOLVED Home origin the popup was OPENED at — always accepted (F3).
19
+ * @param relayChannel the app's BroadcastChannel name (the COOP relay fallback; must match the Home).
20
+ * @param isAllowedOrigin OPTIONAL: also accept a success message from any OTHER origin this predicate
21
+ * trusts as a Home. Needed because the Home may HOP a nameless apex enroll to the member's `<label>`
22
+ * subdomain mid-ceremony (passkey rpId isolation / per-handle home) — the code then arrives from the
23
+ * subdomain, not `expectedOrigin`. Still fail-closed: `e.source === popup` (the exact window we opened)
24
+ * + the echoed `state` (F5) + the single-use PKCE-bound code remain required; we only widen the ORIGIN
25
+ * set to the app's declared Home allowlist, never to "any origin". The result's `authOrigin` reports
26
+ * the origin the code actually came from so the caller exchanges it at the right `iss`.
27
+ * Resolves `blocked` if the popup couldn't open (caller falls back to a full-page redirect, ADR-0013). */
28
+ export function openCentralAuthPopup(redirectUrl, expectedState, expectedOrigin, relayChannel, onProgress, signal, isAllowedOrigin) {
29
+ const originOk = (o) => o === expectedOrigin || (!!isAllowedOrigin && isAllowedOrigin(o));
30
+ const popupUrl = redirectUrl + (redirectUrl.includes('?') ? '&' : '?') + 'mode=popup';
31
+ const w = 480;
32
+ const h = 680;
33
+ const left = Math.max(0, Math.round(window.screenX + (window.outerWidth - w) / 2));
34
+ const top = Math.max(0, Math.round(window.screenY + (window.outerHeight - h) / 2));
35
+ const popup = window.open(popupUrl, 'agentic-connect', `popup=yes,width=${w},height=${h},left=${left},top=${top}`);
36
+ if (!popup)
37
+ return Promise.resolve({ status: 'blocked' });
38
+ const relay = typeof BroadcastChannel !== 'undefined' ? new BroadcastChannel(relayChannel) : null;
39
+ return new Promise((resolve) => {
40
+ let done = false;
41
+ const finish = (r) => {
42
+ if (done)
43
+ return;
44
+ done = true;
45
+ window.removeEventListener('message', onMessage);
46
+ signal?.removeEventListener('abort', onAbort);
47
+ if (relay) {
48
+ relay.onmessage = null;
49
+ try {
50
+ relay.close();
51
+ }
52
+ catch { /* ignore */ }
53
+ }
54
+ clearTimeout(abandonTimeout);
55
+ resolve(r);
56
+ };
57
+ // Under COOP (`same-origin-allow-popups`) the opener's handle is severed once the Home loads, so we
58
+ // CANNOT poll `popup.closed`. Success arrives over the relay (or postMessage if the opener survives);
59
+ // a parent-side Cancel (AbortSignal) or the abandon timeout handles the rest.
60
+ const onAbort = () => { try {
61
+ popup.close();
62
+ }
63
+ catch { /* ignore */ } finish({ status: 'cancelled' }); };
64
+ if (signal?.aborted) {
65
+ onAbort();
66
+ return;
67
+ }
68
+ signal?.addEventListener('abort', onAbort, { once: true });
69
+ if (relay) {
70
+ relay.onmessage = (e) => {
71
+ const m = e.data;
72
+ if (!m || m.kind !== 'ac-relay')
73
+ return;
74
+ if (m.state !== expectedState || !m.code)
75
+ return; // replay / cross-binding guard (F5)
76
+ try {
77
+ popup.close();
78
+ }
79
+ catch { /* ignore */ }
80
+ // `m.iss` (from `?ac_iss=…`) is the origin that minted the code — exchange there, not the opened one.
81
+ finish({ status: 'success', code: m.code, authOrigin: m.iss && originOk(m.iss) ? m.iss : expectedOrigin });
82
+ };
83
+ }
84
+ const onMessage = (e) => {
85
+ if (e.source !== popup || !originOk(e.origin))
86
+ return; // fail-closed (F3): the popup we opened, at a trusted Home origin
87
+ const m = e.data;
88
+ if (!m || typeof m.type !== 'string')
89
+ return;
90
+ if (m.type === 'AC_PROGRESS') {
91
+ if (m.msg)
92
+ onProgress?.(m.msg);
93
+ return;
94
+ }
95
+ if (m.type === 'AC_SUCCESS') {
96
+ if (m.state !== expectedState || !m.code)
97
+ return; // replay / cross-binding guard (F5)
98
+ try {
99
+ popup.close();
100
+ }
101
+ catch { /* ignore */ }
102
+ // The ceremony may have completed on a hopped `<label>` subdomain — `e.origin` is where the code
103
+ // was minted, so the caller exchanges it there (matching the id_token `iss`).
104
+ finish({ status: 'success', code: m.code, authOrigin: e.origin });
105
+ return;
106
+ }
107
+ if (m.type === 'AC_ERROR') {
108
+ try {
109
+ popup.close();
110
+ }
111
+ catch { /* ignore */ }
112
+ finish({ status: 'error', error: m.error ?? 'enrollment failed' });
113
+ return;
114
+ }
115
+ if (m.type === 'AC_CANCEL') {
116
+ try {
117
+ popup.close();
118
+ }
119
+ catch { /* ignore */ }
120
+ finish({ status: 'cancelled' });
121
+ }
122
+ };
123
+ window.addEventListener('message', onMessage);
124
+ // Abandon backstop: with the handle COOP-severed we can't observe a close, so an abandoned popup
125
+ // resolves cancelled after 2 min instead of hanging the caller. Cancel (above) is the fast path.
126
+ const abandonTimeout = window.setTimeout(() => finish({ status: 'cancelled' }), 2 * 60 * 1000);
127
+ });
128
+ }
129
+ /**
130
+ * The POPUP-SIDE half of the COOP relay — call this once at the relying app's entry (`main.tsx`), BEFORE
131
+ * rendering. When the OAuth IdP severed the popup's opener (COOP), the Home redirects this popup back to the
132
+ * relying origin with `?ac_relay=1&code&state` (+ `&ac_iss=<home origin>` when the Home that minted the code
133
+ * is not the origin the popup was opened at — e.g. an apex enroll that hopped to the member's `<label>`
134
+ * subdomain); we (the popup, now same-origin with the opener) hand `{code,state,iss}` to the opener over the
135
+ * same-origin BroadcastChannel `relayChannel` (which `openCentralAuthPopup` listens on) and close. Returns
136
+ * `true` when this load IS a relay return — the caller should then NOT render the app (just relay + close).
137
+ * Returns `false` for a normal load (render as usual). The relay-channel name must match the one passed to
138
+ * `openCentralAuthPopup`. Only `code`+`state`(+`iss` origin) cross the channel — never a token. */
139
+ export function relayCodeIfPopupReturn(relayChannel) {
140
+ if (typeof window === 'undefined')
141
+ return false;
142
+ const u = new URL(window.location.href);
143
+ if (u.searchParams.get('ac_relay') !== '1')
144
+ return false;
145
+ const code = u.searchParams.get('code');
146
+ const state = u.searchParams.get('state');
147
+ const iss = u.searchParams.get('ac_iss') ?? undefined;
148
+ if (code && state && typeof BroadcastChannel !== 'undefined') {
149
+ const ch = new BroadcastChannel(relayChannel);
150
+ ch.postMessage({ kind: 'ac-relay', code, state, iss });
151
+ window.setTimeout(() => { try {
152
+ ch.close();
153
+ }
154
+ catch { /* ignore */ } try {
155
+ window.close();
156
+ }
157
+ catch { /* ignore */ } }, 150);
158
+ }
159
+ return true;
160
+ }
161
+ //# sourceMappingURL=popup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"popup.js","sourceRoot":"","sources":["../src/popup.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@agenticprimitives/connect-client",
3
+ "version": "1.0.0-alpha.1",
4
+ "description": "Generic relying-party connect client: hand off to the central-auth Home (FedCM-first → OIDC redirect/popup), exchange the code for an SA-signed scoped delegation. The relying half of spec 230/259; no credential ever originates in the relying app.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/agentictrustlabs/agenticprimitives.git",
9
+ "directory": "packages/connect-client"
10
+ },
11
+ "homepage": "https://github.com/agentictrustlabs/agenticprimitives/tree/master/packages/connect-client",
12
+ "bugs": {
13
+ "url": "https://github.com/agentictrustlabs/agenticprimitives/issues"
14
+ },
15
+ "type": "module",
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "LICENSE",
26
+ "dist",
27
+ "spec.md",
28
+ "README.md"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "dependencies": {
34
+ "@agenticprimitives/fedcm-rp": "1.0.0-alpha.6",
35
+ "@agenticprimitives/types": "1.0.0-alpha.13"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^22.7.0",
39
+ "vitest": "^4.1.8"
40
+ },
41
+ "keywords": [
42
+ "oidc",
43
+ "relying-party",
44
+ "connect",
45
+ "fedcm",
46
+ "sign-in",
47
+ "agentic"
48
+ ],
49
+ "scripts": {
50
+ "build": "tsc -p tsconfig.build.json",
51
+ "typecheck": "tsc -p tsconfig.json --noEmit",
52
+ "test": "vitest run",
53
+ "test:unit": "vitest run test/unit --passWithNoTests",
54
+ "test:integration": "vitest run test/integration --passWithNoTests",
55
+ "test:watch": "vitest",
56
+ "clean": "rm -rf dist"
57
+ }
58
+ }
package/spec.md ADDED
@@ -0,0 +1,20 @@
1
+ # @agenticprimitives/connect-client — spec
2
+
3
+ The full design lives in
4
+ [`../../specs/295-relying-connect-client.md`](../../specs/295-relying-connect-client.md).
5
+
6
+ This package is the **relying-party half** of Connect (spec 230 / [spec 259](../../specs/259-relying-idp-responsibility-split.md),
7
+ [ADR-0019](../../docs/architecture/decisions/0019-relying-site-authority-is-a-scoped-delegation.md)). A
8
+ relying app originates **no** credential or OAuth request: it hands off to the user's central-auth **Home**
9
+ (the IdP), which runs the credential ceremony (wallet / Google / YouVersion / passkey) + custody and returns
10
+ an OIDC `code`; the relying app exchanges it for an **SA-signed scoped delegation** ([ADR-0041](../../docs/architecture/decisions/0041-web3-authority-not-oauth-on-a2a-to-mcp.md)).
11
+
12
+ ## Scope (this package)
13
+ - The relying-side OIDC protocol: PKCE, `buildAuthorizeUrl`, `startEnrollment`/`startOrgCreation`,
14
+ `exchangeCode` (→ `/token`), `verifyIdToken` (ES256, iss/aud/nonce/exp, issuer allowlist).
15
+ - The seamless launcher: FedCM-first ([ADR-0032](../../docs/architecture/decisions/0032-fedcm-delegation-via-substrate-grant-endpoint.md))
16
+ → origin-validated first-party popup → full-page redirect.
17
+
18
+ ## Out of scope
19
+ - The broker/Home side (`@agenticprimitives/connect`) and the credential ceremonies (`connect-auth`).
20
+ - Any hostname / TLD / origin literal — injected by the app via `ConnectClientConfig` (ADR-0021).