@matanetwork/sovereign-id 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MATA Network
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,251 @@
1
+ # @matanetwork/sovereign-id
2
+
3
+ Page-side SDK for **MATA Sovereign ID** (mID) — the permissionless
4
+ self-issued identity protocol.
5
+
6
+ Drop one button into your sign-in page. Users authenticate with their
7
+ own wallet (browser extension or native app). You get back a signed
8
+ JWT carrying a stable DID + the claims they consented to disclose.
9
+ No `client_id`, no portal account, no MAU pricing, no MATA HTTP
10
+ traffic at runtime.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @matanetwork/sovereign-id
16
+ ```
17
+
18
+ Pair with the backend verifier (separate package, same protocol):
19
+
20
+ ```bash
21
+ npm install @matanetwork/sovereign-id-verify
22
+ ```
23
+
24
+ ## Quick start
25
+
26
+ ```javascript
27
+ import { signIn, resumePendingSignIn, SignInError } from '@matanetwork/sovereign-id';
28
+
29
+ // 1. On boot — resume an interrupted sign-in if one was stashed.
30
+ // Returns null when there's nothing pending; the normal page renders.
31
+ resumePendingSignIn().then(handleResult);
32
+
33
+ // 2. On sign-in button click — start a fresh request.
34
+ document.getElementById('signin').addEventListener('click', async () => {
35
+ try {
36
+ const nonce = await fetch('/api/auth/nonce').then(r => r.text());
37
+ const result = await signIn({
38
+ rpOrigin: 'https://acme.com',
39
+ nonce,
40
+ claims: {
41
+ required: ['did'],
42
+ optional: ['email', 'name'],
43
+ },
44
+ });
45
+ handleResult(result);
46
+ } catch (err) {
47
+ if (err instanceof SignInError) handleSignInError(err);
48
+ else throw err;
49
+ }
50
+ });
51
+
52
+ async function handleResult(result) {
53
+ if (!result) return; // resumePendingSignIn returned null — nothing to do.
54
+
55
+ // Hand the JWT to your backend for verification.
56
+ const resp = await fetch('/api/auth/mid', {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/json' },
59
+ body: JSON.stringify({ jwt: result.jwt }),
60
+ });
61
+ if (resp.ok) window.location.href = '/dashboard';
62
+ }
63
+
64
+ function handleSignInError(err) {
65
+ switch (err.code) {
66
+ case 'user_denied': /* user clicked Deny on the consent screen */ break;
67
+ case 'upsell_canceled': /* user dismissed the install upsell */ break;
68
+ case 'timeout': /* user left the consent screen open too long */ break;
69
+ case 'origin_mismatch': /* page lied about its origin (rare) */ break;
70
+ default: console.error(err.code, err.message);
71
+ }
72
+ }
73
+ ```
74
+
75
+ That's it. No back-channel `/token` exchange. No JWKS to refresh. The
76
+ JWT is self-anchored — your backend verifies it entirely against
77
+ cryptographic material embedded in the token.
78
+
79
+ ## API reference
80
+
81
+ ### `signIn(request, options?)`
82
+
83
+ Probes for the MATA browser extension first; falls back to the
84
+ `mata-mid://` native-app deep link. If neither responds, an install
85
+ upsell modal opens (links to the Chrome Web Store, polls for the
86
+ extension, auto-resumes once installed).
87
+
88
+ #### `request` — required
89
+
90
+ | Field | Type | Notes |
91
+ |---|---|---|
92
+ | `rpOrigin` | `string` | Your bare origin, e.g. `"https://acme.com"`. Becomes the JWT's `aud` claim. |
93
+ | `nonce` | `string` | Single-use random string from your backend. Echoed in the JWT for replay defense. |
94
+ | `claims.required` | `string[]` | Claim keys the user MUST approve. Almost always `["did"]`. Denial blocks sign-in. |
95
+ | `claims.optional` | `string[]` | Claim keys the user can include or skip. |
96
+ | `claims.custom` | `Record<string, {optional: true, description?: string}>` | Arbitrary keys from the user's `profile_kv` (v0: ignored by the wallet). |
97
+
98
+ **Standard claim catalog (v1):** `did`, `email`, `name`, `created_at`,
99
+ `paired_devices_count`, `level_rating.trust`, `level_rating.security`,
100
+ `level_rating.incentive`.
101
+
102
+ #### `options` — optional
103
+
104
+ | Field | Type | Default | Notes |
105
+ |---|---|---|---|
106
+ | `timeoutMs` | `number` | `120000` | Hard request timeout. |
107
+ | `nativeAppCallback` | `string` | `window.location.href` | URL the native app's callback redirects to. |
108
+ | `installUpsell` | `boolean` | `true` | When `false`, `ERR_NO_WALLET_INSTALLED` is thrown raw instead of showing the modal. |
109
+ | `ref` | `string \| null` | hostname of `rpOrigin` | Referral code stamped onto the upsell's outbound links. Default delivers attribution back to your domain. Pass `null` to opt out. |
110
+
111
+ #### Returns
112
+
113
+ `Promise<{ jwt: string, surface: 'extension' | 'native_app' }>`.
114
+
115
+ #### Throws
116
+
117
+ `SignInError` with `.code` one of:
118
+
119
+ | Code | When |
120
+ |---|---|
121
+ | `user_denied` | User clicked Deny on the consent screen. |
122
+ | `upsell_canceled` | User dismissed the install upsell ("Cancel" or Escape). |
123
+ | `no_wallet_installed` | Only when `installUpsell: false`. Neither extension nor native app responded. |
124
+ | `invalid_request` | Your code passed a malformed request (caught on the page). |
125
+ | `origin_mismatch` | Page-claimed origin didn't match the actual tab origin. Possible page bug or attack. |
126
+ | `wallet_unavailable` | Vault locked, wallet not bootstrapped, or no matching credential. |
127
+ | `required_claim_unavailable` | User signed up without an email but you required it. |
128
+ | `timeout` | User didn't decide within `timeoutMs`. |
129
+ | `internal_error` | Anything else. |
130
+
131
+ ### `resumePendingSignIn()`
132
+
133
+ Call once at app boot. Resumes a sign-in that was interrupted by a
134
+ page reload during the install upsell.
135
+
136
+ | Returns | When |
137
+ |---|---|
138
+ | `{ jwt, surface }` | A pending request was stashed, the extension is now installed, and the resumed sign-in completed. |
139
+ | `null` | No pending request, or the stash is stale, or the extension is still missing. |
140
+ | `throws SignInError` | A pending request exists and the extension is present, but the resumed sign-in itself failed. |
141
+
142
+ The stash lives in `sessionStorage` (per-tab, cleared on tab close).
143
+ TTL = the original request's `timeoutMs`. Stale entries are silently
144
+ dropped on read.
145
+
146
+ ### `hasExtension()`
147
+
148
+ Returns `true` when `window.__mata_mid__` (set by the extension's
149
+ content script) is present. Useful for conditionally rendering the
150
+ sign-in button vs. an install prompt before the user has clicked.
151
+
152
+ > **Race-condition note.** The content script runs at `document_idle`.
153
+ > RPs calling `signIn()` synchronously from `<head>` may probe before
154
+ > the script is injected. Call after `DOMContentLoaded` (or after a
155
+ > user gesture) to avoid the false negative.
156
+
157
+ ### `showInstallUpsell(options)`
158
+
159
+ Exposed for RPs who set `installUpsell: false` and want to render the
160
+ upsell on their own conditions (e.g. after their own UI flow). Same
161
+ modal `signIn()` uses internally; same `'installed' | 'canceled'`
162
+ result shape.
163
+
164
+ ### `pickInstallCta()` / `defaultRefFromOrigin(origin)`
165
+
166
+ Helpers for RPs building their own install upsell UI:
167
+
168
+ - `pickInstallCta()` returns the right `{label, url, hint}` for the
169
+ current browser/OS.
170
+ - `defaultRefFromOrigin('https://acme.com')` returns `'acme.com'` — the
171
+ referral code the SDK would stamp by default.
172
+
173
+ ### `clearPendingSignIn()`
174
+
175
+ Imperatively drops the resume stash. Use when you've navigated to a
176
+ different sign-in flow that supersedes the pending mID request.
177
+
178
+ ### `SignInError`
179
+
180
+ ```typescript
181
+ class SignInError extends Error {
182
+ readonly code: ErrorCode;
183
+ }
184
+ ```
185
+
186
+ ## Referral attribution
187
+
188
+ Both outbound links in the install upsell modal carry `?ref=<code>` by
189
+ default — your domain becomes the attribution code without any extra
190
+ wiring. The signup flow at my.mata.network reads `?ref=` and forwards
191
+ it through to your downstream analytics as `referral_code`.
192
+
193
+ Customize:
194
+
195
+ ```javascript
196
+ signIn(request, { ref: 'acme-launch-2026' }); // custom code
197
+ signIn(request, { ref: null }); // opt out
198
+ ```
199
+
200
+ ## What you don't have to think about
201
+
202
+ - **No `client_id`, `client_secret`, redirect URI allowlist.** There's no registration step.
203
+ - **No `/token` back-channel exchange.** The JWT comes back from `signIn()` directly.
204
+ - **No JWKS endpoint refresh.** The JWT bundles its own resolution data.
205
+ - **No DID-resolver HTTP calls.** The DID is its own public key.
206
+ - **No MAU pricing.** No metering of any kind.
207
+
208
+ ## Verification on the backend
209
+
210
+ Use [@matanetwork/sovereign-id-verify](https://npmjs.com/package/@matanetwork/sovereign-id-verify):
211
+
212
+ ```javascript
213
+ import { verifyResponse } from '@matanetwork/sovereign-id-verify';
214
+
215
+ const verified = await verifyResponse(req.body.jwt, {
216
+ expectedAudience: 'https://acme.com',
217
+ expectedNonce: sessionNonce,
218
+ nowUnixSecs: Math.floor(Date.now() / 1000),
219
+ });
220
+
221
+ // verified.did — stable user identifier
222
+ // verified.claims — disclosed values
223
+ // verified.currentVersion — head roster version; cache for rollback detection
224
+ ```
225
+
226
+ ## Browser support
227
+
228
+ | Browser | Sign-in path |
229
+ |---|---|
230
+ | Chrome / Edge / Brave / Arc / Opera | Extension or native-app deep link |
231
+ | Safari | Native-app deep link (extension coming) |
232
+ | Firefox | Native-app deep link (extension coming) |
233
+ | Mobile Chrome / Safari | Native-app deep link (apps coming) |
234
+
235
+ When no compatible surface is available, the install upsell modal
236
+ surfaces the right CTA per browser/OS automatically.
237
+
238
+ ## Accessibility
239
+
240
+ The install upsell modal:
241
+
242
+ - Renders in a closed Shadow DOM (RP CSS can't break or skin it).
243
+ - Traps Tab / Shift+Tab focus inside the modal.
244
+ - Restores focus to the element that was focused before opening.
245
+ - Closes on Escape and on backdrop click.
246
+ - Shows `:focus-visible` outlines for keyboard users.
247
+ - Uses `role="dialog"` + `aria-modal="true"` + `aria-labelledby`.
248
+
249
+ ## License
250
+
251
+ MIT — see [LICENSE](LICENSE).
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@matanetwork/sovereign-id",
3
+ "version": "0.1.0",
4
+ "description": "Page-side SDK for MATA Sovereign ID — the permissionless self-issued identity protocol (mID). Probes for the MATA browser extension or native app, dispatches sign-in requests, and resolves with a signed JWT.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "types": "src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.d.ts",
11
+ "import": "./src/index.js"
12
+ }
13
+ },
14
+ "sideEffects": false,
15
+ "scripts": {
16
+ "test": "node --test tests/index.test.js",
17
+ "prepublishOnly": "node --test tests/index.test.js"
18
+ },
19
+ "files": [
20
+ "src/",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "keywords": [
25
+ "mata",
26
+ "sovereign-id",
27
+ "mid",
28
+ "sso",
29
+ "identity",
30
+ "did",
31
+ "self-sovereign",
32
+ "passwordless",
33
+ "did-mata",
34
+ "permissionless-auth"
35
+ ],
36
+ "license": "MIT",
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "homepage": "https://github.com/mata-network/mata/tree/main/packages/mata-sovereign-id-sdk#readme",
41
+ "bugs": {
42
+ "url": "https://github.com/mata-network/mata/issues"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/mata-network/mata.git",
47
+ "directory": "packages/mata-sovereign-id-sdk"
48
+ }
49
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Type declarations for @matanetwork/sovereign-id.
3
+ *
4
+ * The runtime is pure JS (no TypeScript build step). This `.d.ts`
5
+ * exists so consumers using TypeScript get full type-checking +
6
+ * IDE intellisense.
7
+ */
8
+
9
+ // ─── Wire protocol constants ───────────────────────────────────────────────
10
+
11
+ export const WINDOW_MID_GLOBAL: '__mata_mid__';
12
+ export const MESSAGE_DISCRIMINATOR: '__mata_mid_v1';
13
+ export const KIND_SIGN_IN_REQUEST: 'sign_in_request';
14
+ export const KIND_SIGN_IN_RESPONSE: 'sign_in_response';
15
+ export const URL_SCHEME: 'mata-mid';
16
+ export const SCHEME_PATH_REQUEST: 'request';
17
+ export const QUERY_PARAM_PAYLOAD: 'payload';
18
+ export const FRAGMENT_KEY_RESPONSE: 'mid_response';
19
+ export const PROTOCOL_VERSION: 1;
20
+
21
+ // ─── Standard error codes ──────────────────────────────────────────────────
22
+
23
+ export const ERR_USER_DENIED: 'user_denied';
24
+ export const ERR_ORIGIN_MISMATCH: 'origin_mismatch';
25
+ export const ERR_INVALID_REQUEST: 'invalid_request';
26
+ export const ERR_WALLET_UNAVAILABLE: 'wallet_unavailable';
27
+ export const ERR_REQUIRED_CLAIM_UNAVAILABLE: 'required_claim_unavailable';
28
+ export const ERR_INTERNAL: 'internal_error';
29
+ export const ERR_NO_WALLET_INSTALLED: 'no_wallet_installed';
30
+ export const ERR_TIMEOUT: 'timeout';
31
+ export const ERR_UPSELL_CANCELED: 'upsell_canceled';
32
+
33
+ export type ErrorCode =
34
+ | typeof ERR_USER_DENIED
35
+ | typeof ERR_ORIGIN_MISMATCH
36
+ | typeof ERR_INVALID_REQUEST
37
+ | typeof ERR_WALLET_UNAVAILABLE
38
+ | typeof ERR_REQUIRED_CLAIM_UNAVAILABLE
39
+ | typeof ERR_INTERNAL
40
+ | typeof ERR_NO_WALLET_INSTALLED
41
+ | typeof ERR_TIMEOUT
42
+ | typeof ERR_UPSELL_CANCELED;
43
+
44
+ // ─── Request / response types ──────────────────────────────────────────────
45
+
46
+ export interface CustomClaim {
47
+ optional: true;
48
+ description?: string;
49
+ }
50
+
51
+ export interface SignInRequest {
52
+ /** The RP's bare origin, e.g. `"https://acme.com"`. */
53
+ rpOrigin: string;
54
+ /** RP-issued single-use nonce; appears in the JWT for replay defense. */
55
+ nonce: string;
56
+ /** Claim catalog the wallet should disclose. */
57
+ claims: {
58
+ /** Claims that MUST be approved; denial blocks sign-in. */
59
+ required: string[];
60
+ /** Claims the user can include or skip. */
61
+ optional?: string[];
62
+ /** Arbitrary custom keys from the user's profile_kv (v0: ignored). */
63
+ custom?: Record<string, CustomClaim>;
64
+ };
65
+ }
66
+
67
+ export interface SignInOptions {
68
+ /** Hard request timeout. Default: 120 seconds. */
69
+ timeoutMs?: number;
70
+ /**
71
+ * URL the native app's response will redirect to. Default:
72
+ * `window.location.href`. Only honored when the SDK falls through
73
+ * to the native-app deep link.
74
+ */
75
+ nativeAppCallback?: string;
76
+ /**
77
+ * Whether to show the install upsell modal when no wallet is
78
+ * detected on the user's device. Default: `true`. Set to `false`
79
+ * to get a raw `ERR_NO_WALLET_INSTALLED` rejection and handle
80
+ * the upsell UI yourself.
81
+ */
82
+ installUpsell?: boolean;
83
+ /**
84
+ * Referral code attributed to signups that flow through the install
85
+ * upsell. Default: the hostname extracted from `rpOrigin` (e.g.
86
+ * `"acme.com"`), so RPs get attribution by default without any
87
+ * extra wiring. Pass `null` to opt out of attribution entirely, or
88
+ * a custom string to override (e.g. a configured MATA referral
89
+ * code your team uses).
90
+ *
91
+ * Stamped onto the install CTA and the "create your account" link
92
+ * inside the upsell modal as `?ref=<code>`, following the existing
93
+ * MATA referral convention captured by my.mata.network's welcome
94
+ * view + signup form.
95
+ */
96
+ ref?: string | null;
97
+ }
98
+
99
+ /**
100
+ * One of the CTAs the install upsell can present. Pick by browser/OS.
101
+ * Exposed for RPs that want to render their own upsell UI from the
102
+ * same recommendation engine.
103
+ */
104
+ export interface InstallCta {
105
+ /** Button text (e.g. `"Install MATA for Chrome"`). */
106
+ label: string;
107
+ /** Where the button navigates to. */
108
+ url: string;
109
+ /** Sub-text under the CTA (e.g. `"Opens the Chrome Web Store in a new tab"`). */
110
+ hint: string;
111
+ }
112
+
113
+ export interface InstallUpsellOptions {
114
+ /** Shown prominently as the requesting RP. */
115
+ rpOrigin: string;
116
+ /**
117
+ * Inversion-of-control hook so the SDK's `hasExtension()` is used
118
+ * in production and tests can stub it.
119
+ */
120
+ hasExtensionFn?: () => boolean;
121
+ /** Override the auto-picked CTA. */
122
+ cta?: InstallCta;
123
+ /** Default 1000 ms. */
124
+ pollIntervalMs?: number;
125
+ /**
126
+ * Referral code attributed to signups that flow through this
127
+ * upsell. Default: hostname of `rpOrigin`. Pass `null` to disable.
128
+ * See `SignInOptions.ref` for the full attribution model.
129
+ */
130
+ ref?: string | null;
131
+ }
132
+
133
+ export type InstallUpsellResult = 'installed' | 'canceled';
134
+
135
+ export interface SignInSuccess {
136
+ /** JWS compact-form mID token. */
137
+ jwt: string;
138
+ /** Which surface produced the JWT. */
139
+ surface: 'extension' | 'native_app';
140
+ }
141
+
142
+ export class SignInError extends Error {
143
+ constructor(code: ErrorCode, message: string);
144
+ readonly code: ErrorCode;
145
+ }
146
+
147
+ // ─── Public functions ──────────────────────────────────────────────────────
148
+
149
+ export function signIn(
150
+ request: SignInRequest,
151
+ options?: SignInOptions
152
+ ): Promise<SignInSuccess>;
153
+
154
+ export function hasExtension(): boolean;
155
+
156
+ /** Internal — exposed for SDK consumers that need to roll their own. */
157
+ export function generateRequestId(): string;
158
+ /** Internal — exposed for the verifier package + tests. */
159
+ export function base64UrlEncode(str: string): string;
160
+ /** Internal — exposed for the verifier package + tests. */
161
+ export function base64UrlDecode(b64url: string): string;
162
+
163
+ /**
164
+ * Pick the right install CTA for the user's browser / OS. Auto-called
165
+ * by `signIn()` when the upsell fires; exposed so RPs that opted out
166
+ * of the inline modal can use the same recommendation engine in
167
+ * their own UI.
168
+ */
169
+ export function pickInstallCta(): InstallCta;
170
+
171
+ /**
172
+ * Extract the default referral code from an RP origin — the bare
173
+ * hostname (e.g. `"https://acme.com"` → `"acme.com"`). Exposed so
174
+ * RPs can preview what attribution string they'd get without
175
+ * actually triggering the upsell.
176
+ *
177
+ * Returns `null` for malformed origins.
178
+ */
179
+ export function defaultRefFromOrigin(rpOrigin: string): string | null;
180
+
181
+ /**
182
+ * Render the install upsell overlay and resolve when the user either
183
+ * cancels or completes the install. Auto-called by `signIn()` on
184
+ * `ERR_NO_WALLET_INSTALLED`; exposed for RPs who set
185
+ * `installUpsell: false` and want to invoke it on their own
186
+ * conditions.
187
+ */
188
+ export function showInstallUpsell(
189
+ options: InstallUpsellOptions
190
+ ): Promise<InstallUpsellResult>;
191
+
192
+ /**
193
+ * Resume a sign-in that was interrupted by a page reload during the
194
+ * install upsell.
195
+ *
196
+ * Call once at app boot — ideally as early as possible — so a user
197
+ * who installed MATA and reloaded sees their sign-in continue without
198
+ * a visible flicker through the logged-out state.
199
+ *
200
+ * - Returns `{jwt, surface}` if a pending request was found, the
201
+ * extension is now installed, and the resumed sign-in completed.
202
+ * - Returns `null` if no resume is pending, the stash is stale, or
203
+ * the extension is still missing (the user reloaded before
204
+ * actually installing).
205
+ * - Rejects with `SignInError` when a pending request exists and the
206
+ * extension is present but the sign-in itself failed (`user_denied`,
207
+ * `timeout`, etc.). Same error shape as `signIn()`.
208
+ */
209
+ export function resumePendingSignIn(): Promise<SignInSuccess | null>;
210
+
211
+ /**
212
+ * Imperatively drop any pending resume entry. Useful when the RP
213
+ * has navigated to a different sign-in flow that supersedes the
214
+ * pending mID request.
215
+ */
216
+ export function clearPendingSignIn(): void;