@metalabel/dfos-client 0.36.0 → 0.37.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/README.md +7 -11
- package/dist/siwd.d.ts +39 -10
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ import { indexedDbStore, memoryStore } from '@metalabel/dfos-client/store';
|
|
|
77
77
|
|
|
78
78
|
### `@metalabel/dfos-client/api-auth`
|
|
79
79
|
|
|
80
|
-
API Authentication request proofs
|
|
80
|
+
API Authentication request proofs — the per-request proof-of-possession envelope defined by the [API-AUTH specification](https://protocol.dfos.com/api-auth), which owns the semantics; this subpath signs and verifies it.
|
|
81
81
|
|
|
82
82
|
**Spending a credential: a signing `fetch`.** Hand it to any API client with a fetch seam, and every request that client composes goes out credential-gated.
|
|
83
83
|
|
|
@@ -100,7 +100,7 @@ Three consequences worth knowing before you wire it up:
|
|
|
100
100
|
- **It does not follow redirects** (`redirect: 'manual'`): a 3xx comes back to you as-is, because following it would re-issue the request at coordinates the proof does not cover and carry `X-Credential` to whatever authority the `Location` names.
|
|
101
101
|
- **It buffers the request body before sending.** The proof covers the whole body, so there is nothing to sign until the last octet is in hand — size-bounded requests only. An unbounded or live stream cannot be proof-signed, in any implementation.
|
|
102
102
|
|
|
103
|
-
**A backend that must not proxy uses the decomposed form.** A signing backend fronting a browser
|
|
103
|
+
**A backend that must not proxy uses the decomposed form.** A signing backend fronting a browser describes the one request it is willing to make — never signing coordinates the browser supplies ([API-AUTH § Security Considerations](https://protocol.dfos.com/api-auth#security-considerations)) — so there is no `Request` for the adapter above to cover:
|
|
104
104
|
|
|
105
105
|
```typescript
|
|
106
106
|
import { buildApiAuthHeaders, signApiRequest } from '@metalabel/dfos-client/api-auth';
|
|
@@ -136,7 +136,7 @@ It throws `ApiRequestVerifyError`, carrying `reason` (`invalid` / `unverifiable`
|
|
|
136
136
|
|
|
137
137
|
### `@metalabel/dfos-client/siwd`
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
There are two end-to-end integration recipes, and which one you want depends on where your application runs. An app with a domain (mint an app identity, serve the app description, verify the callback) follows <https://docs.dfos.com/docs/developers/sign-in-with-dfos/setup>. A CLI or agent running on the user's own machine, redirecting to a loopback port, follows <https://docs.dfos.com/docs/developers/sign-in-with-dfos/local-apps>.
|
|
140
140
|
|
|
141
141
|
```typescript
|
|
142
142
|
import {
|
|
@@ -149,7 +149,7 @@ import {
|
|
|
149
149
|
|
|
150
150
|
Sign In With DFOS. The three verbs above are the relying-party login kit, in the order a login uses them: `createSiwdLoginRequest` mints the challenge and builds the `/authorize` URL to redirect to, `readSiwdCallback` parses what comes back, and `verifySiwd` verifies it — mint → redirect, read → verify. The `expect` object `createSiwdLoginRequest` returns (nonce, domain, and the DID when the challenge is bound to one) is what `verifySiwd` checks against, so the relying party MUST persist it across the redirect: a verifier that takes its expectation from the callback has implemented the check and none of the protection. See [`examples/siwd-demo`](../../examples/siwd-demo) for the reference consumer.
|
|
151
151
|
|
|
152
|
-
The `nonce`/`consumeNonce` pair on the expectation (supply exactly one)
|
|
152
|
+
The `nonce`/`consumeNonce` pair on the expectation (supply exactly one) maps one field each to the spec's two replay disciplines — which discipline a given scope obliges, and why, is [SIWD § Replay prevention](https://protocol.dfos.com/siwd#replay-prevention)'s argument to make:
|
|
153
153
|
|
|
154
154
|
**`expect.nonce` — flow-bound login.** For a backend granting only a browser session (`scope=identity`), source the expected nonce from state you bound to that browser at mint time — a server-side session, or the nonce sealed under your own key in an `httpOnly` cookie — and compare:
|
|
155
155
|
|
|
@@ -159,9 +159,7 @@ The `nonce`/`consumeNonce` pair on the expectation (supply exactly one) is the s
|
|
|
159
159
|
await verifySiwd(client, jws, { domain, nonce });
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
**`consumeNonce` — spend the nonce.** Required the moment success grants anything beyond a session with the presenting browser (a credential-returning scope, a portable token, a profile-B mailbox flow):
|
|
162
|
+
**`consumeNonce` — spend the nonce.** Required the moment success grants anything beyond a session with the presenting browser (a credential-returning scope, a portable token, a profile-B mailbox flow). Return true iff **this** verifier minted the nonce and it was unspent, deleting it in the same atomic operation (a Redis `GETDEL`, a `DELETE … RETURNING` — never get-then-delete):
|
|
165
163
|
|
|
166
164
|
```typescript
|
|
167
165
|
await verifySiwd(client, jws, {
|
|
@@ -170,13 +168,11 @@ await verifySiwd(client, jws, {
|
|
|
170
168
|
});
|
|
171
169
|
```
|
|
172
170
|
|
|
173
|
-
|
|
171
|
+
Under either discipline `verifySiwd` checks the nonce at most once, and only after every other check has passed, so an invalid presentation can never burn a nonce the user is still holding.
|
|
174
172
|
|
|
175
173
|
`createSiwdLoginRequest` throws rather than returning an error on the two things that are RP misconfiguration: an `authorizeUrl` or `redirectUri` that is not an absolute URL, and any scope other than `identity` over a loopback redirect that names no client identity.
|
|
176
174
|
|
|
177
|
-
**Loopback redirects** — `http://localhost`, `http://127.0.0.1`, or `http://[::1]`, on any port — come in two shapes
|
|
178
|
-
|
|
179
|
-
The fragment is what keeps the credential off every server, and it is also why a CLI needs one extra step: a browser does not send the fragment to your loopback listener either, so the request line your local server sees carries the query and nothing else. Answer it with a small page whose script reads `location.href` and posts the whole URL back to your server, then feed _that_ to `readSiwdCallback`. A browser relying party just passes `location.href`.
|
|
175
|
+
**Loopback redirects** — `http://localhost`, `http://127.0.0.1`, or `http://[::1]`, on any port — come in two shapes: the **anonymous** one (no `client_did`, `scope=identity` only) and the **key-proven** one, the [loopback credential tier](https://protocol.dfos.com/siwd#loopback-clients), which is what lets local software receive a credential — the spec defines both. The one integration consequence to know: a credential comes back in the URL **fragment**, which a browser sends to no server — your loopback listener's request line included — so a CLI answers the callback with a small page whose script reads `location.href` and posts the whole URL back, then feeds _that_ to `readSiwdCallback`. A browser relying party just passes `location.href`.
|
|
180
176
|
|
|
181
177
|
```typescript
|
|
182
178
|
import {
|
package/dist/siwd.d.ts
CHANGED
|
@@ -87,7 +87,13 @@ interface SiwdLoginRequestInput {
|
|
|
87
87
|
domain: string;
|
|
88
88
|
/** Exact redirect target; must match the RP's registered or served allowlist. */
|
|
89
89
|
redirectUri: string;
|
|
90
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Requested scope: a space-separated SET of scope tokens (the OAuth `scope`
|
|
92
|
+
* convention), each of which must be one specs/SIWD.md §Scopes and Credentials
|
|
93
|
+
* registers. A request naming an unregistered token is refused WHOLE rather
|
|
94
|
+
* than partially honored — a consent screen that silently dropped a token
|
|
95
|
+
* would describe something other than what was asked for.
|
|
96
|
+
*/
|
|
91
97
|
scope: string;
|
|
92
98
|
/** Consent-screen prose. A host MAY decline to render it; see specs/SIWD.md. */
|
|
93
99
|
statement?: string;
|
|
@@ -222,6 +228,14 @@ declare const MAX_SIWD_CLIENT_CHAIN_OPS = 100;
|
|
|
222
228
|
* FULL ordered operation log, genesis first, as base64url of its JSON array —
|
|
223
229
|
* the same grammar as the `challenge` param, so one decoder shape serves both.
|
|
224
230
|
*
|
|
231
|
+
* THIS IS THE LOOPBACK CARRIAGE FORM, and only that. An application that holds a
|
|
232
|
+
* domain encodes nothing: it publishes the very same log as the raw JSON array
|
|
233
|
+
* of the `identity_chain` member of its `/.well-known/dfos-app.json` app
|
|
234
|
+
* description (SIWD.md §`identity_chain` — chain carriage), where the origin
|
|
235
|
+
* serving the file is what associates the domain with the DID. Same chain, same
|
|
236
|
+
* carriage rules — a URL is simply the carrier available to software that holds
|
|
237
|
+
* no origin to publish from.
|
|
238
|
+
*
|
|
225
239
|
* The DID derived from the genesis operation MUST equal the `client_did` the
|
|
226
240
|
* request names; a request where the two disagree makes no claim at all and the
|
|
227
241
|
* host refuses it WHOLE rather than ingesting the chain and ignoring the
|
|
@@ -242,7 +256,12 @@ declare const MAX_SIWD_CLIENT_CHAIN_OPS = 100;
|
|
|
242
256
|
declare const encodeSiwdClientChain: (log: string[]) => string;
|
|
243
257
|
interface SiwdClientIdentity {
|
|
244
258
|
did: string;
|
|
245
|
-
/**
|
|
259
|
+
/**
|
|
260
|
+
* Verbatim identity-op JWS log, genesis first. A loopback client feeds it to
|
|
261
|
+
* `encodeSiwdClientChain` to carry on the authorize URL; an application that
|
|
262
|
+
* holds a domain serves this same array verbatim as the `identity_chain`
|
|
263
|
+
* member of its app description document.
|
|
264
|
+
*/
|
|
246
265
|
chain: string[];
|
|
247
266
|
/** DID URL of the current auth key — feed to signSiwdAskProof. */
|
|
248
267
|
kid: string;
|
|
@@ -251,11 +270,19 @@ interface SiwdClientIdentity {
|
|
|
251
270
|
privateKey: Uint8Array;
|
|
252
271
|
}
|
|
253
272
|
/**
|
|
254
|
-
* Mint a fresh client identity
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
273
|
+
* Mint a fresh client identity: one Ed25519 keypair and a single genesis
|
|
274
|
+
* `create` operation naming it as the auth, assertion, and controller key. That
|
|
275
|
+
* is an entire application identity — a DID whose key control is provable, and a
|
|
276
|
+
* one-operation chain small enough to travel anywhere a chain has to travel.
|
|
277
|
+
*
|
|
278
|
+
* TIER-AGNOSTIC. What comes back is an ordinary DFOS identity, not a
|
|
279
|
+
* loopback-only artifact; the tier is a property of how the identity is
|
|
280
|
+
* PRESENTED, not of the identity itself. An application that holds a domain
|
|
281
|
+
* serves `chain` verbatim as the `identity_chain` member of its
|
|
282
|
+
* `/.well-known/dfos-app.json` app description and names `did` as its
|
|
283
|
+
* `client_did` there; a loopback client carries the same chain on the authorize
|
|
284
|
+
* request instead (`createSiwdLoopbackLoginRequest`), because it has no origin
|
|
285
|
+
* to publish from. Both are minted here.
|
|
259
286
|
*
|
|
260
287
|
* KEY CUSTODY IS THE CALLER'S, and the identity is only as durable as the
|
|
261
288
|
* custody: persist `privateKey` and `chain` — an OS keychain, a file the caller
|
|
@@ -295,9 +322,11 @@ interface SiwdLoopbackLoginRequestInput {
|
|
|
295
322
|
/** Loopback redirect target — `http://` on localhost / 127.0.0.1 / [::1], any port/path. */
|
|
296
323
|
redirectUri: string;
|
|
297
324
|
/**
|
|
298
|
-
* Requested scope
|
|
299
|
-
*
|
|
300
|
-
*
|
|
325
|
+
* Requested scope, in the same space-separated set form as
|
|
326
|
+
* `SiwdLoginRequestInput.scope`. Naming a client identity opens the tier, so
|
|
327
|
+
* every scope the HOST offers is available here — the `identity`-only bound
|
|
328
|
+
* belongs to the anonymous loopback shape, which has no `client_did` to issue
|
|
329
|
+
* a credential to.
|
|
301
330
|
*/
|
|
302
331
|
scope: string;
|
|
303
332
|
/** Consent-screen prose. A host MAY decline to render it; see specs/SIWD.md. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metalabel/dfos-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DFOS Client — the client-side kit for participating in the protocol: resolve, verify, prove. Fetch, resolve, verify-orchestration and cache over untrusted relays, plus the SIWD and API-AUTH proof surfaces. Holds no keys; all crypto truth comes from @metalabel/dfos-protocol",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,15 +47,15 @@
|
|
|
47
47
|
"README.md"
|
|
48
48
|
],
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@metalabel/dfos-protocol": "^0.
|
|
51
|
-
"@metalabel/dfos-web-relay": "^0.
|
|
50
|
+
"@metalabel/dfos-protocol": "^0.37.0",
|
|
51
|
+
"@metalabel/dfos-web-relay": "^0.37.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "^24.10.4",
|
|
55
55
|
"tsup": "^8.5.1",
|
|
56
56
|
"vitest": "^4.1.8",
|
|
57
|
-
"@metalabel/dfos-
|
|
58
|
-
"@metalabel/dfos-
|
|
57
|
+
"@metalabel/dfos-protocol": "0.37.0",
|
|
58
|
+
"@metalabel/dfos-web-relay": "0.37.0"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "tsup",
|