@metalabel/dfos-client 0.29.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 +21 -0
- package/README.md +88 -0
- package/dist/chunk-63XCFYOE.js +16 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +766 -0
- package/dist/memory-CL1DM6Ud.d.ts +5 -0
- package/dist/siwd.d.ts +130 -0
- package/dist/siwd.js +260 -0
- package/dist/store/index.d.ts +14 -0
- package/dist/store/index.js +49 -0
- package/dist/types-ByxTj1u-.d.ts +324 -0
- package/package.json +70 -0
package/dist/siwd.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Signer, VerifiedIdentity, VerifiedSignRequest } from '@metalabel/dfos-protocol/chain';
|
|
2
|
+
import { a as Client, V as VerifyResult } from './types-ByxTj1u-.js';
|
|
3
|
+
import '@metalabel/dfos-protocol/credentials';
|
|
4
|
+
import '@metalabel/dfos-web-relay/peer-client';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The normative JWS header `typ` for a signed SIWD challenge (SIWD.md). Signers
|
|
8
|
+
* MUST set it; `verifySiwd` rejects anything else — it is also what lets typ-
|
|
9
|
+
* routing dispatchers tell a SIWD proof apart from credentials and chain ops.
|
|
10
|
+
*/
|
|
11
|
+
declare const SIWD_JWS_TYP = "did:dfos:siwd";
|
|
12
|
+
interface SiwdChallenge {
|
|
13
|
+
/** Origin domain of the requesting application. */
|
|
14
|
+
domain: string;
|
|
15
|
+
/** Unique replay-prevention value generated by the third party. */
|
|
16
|
+
nonce: string;
|
|
17
|
+
/** ISO 8601 timestamp of challenge creation. */
|
|
18
|
+
timestamp: string;
|
|
19
|
+
/** Human-readable description shown on the consent screen. */
|
|
20
|
+
statement?: string;
|
|
21
|
+
/** Binds the challenge to a specific DID. */
|
|
22
|
+
did?: string;
|
|
23
|
+
}
|
|
24
|
+
interface SiwdSession {
|
|
25
|
+
did: string;
|
|
26
|
+
domain: string;
|
|
27
|
+
nonce: string;
|
|
28
|
+
timestamp: string;
|
|
29
|
+
statement?: string;
|
|
30
|
+
/** The DID URL of the key that signed the challenge. */
|
|
31
|
+
kid: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The byte contract. Serializes a challenge to the canonical bytes that ARE the
|
|
35
|
+
* JWS payload and the base64url query-param body — a fixed key order
|
|
36
|
+
* (domain, nonce, timestamp, statement?, did?) so both halves agree exactly.
|
|
37
|
+
* PURE and clientless: import it in an edge minter and in a browser wallet alike.
|
|
38
|
+
*/
|
|
39
|
+
declare const siwdSigningInput: (challenge: SiwdChallenge) => Uint8Array;
|
|
40
|
+
/**
|
|
41
|
+
* Parse exact SIWD challenge octets under the signer-side WYSIWYS contract.
|
|
42
|
+
* The input must be UTF-8 JSON, satisfy the closed challenge schema, and equal
|
|
43
|
+
* the canonical serialization byte for byte.
|
|
44
|
+
*/
|
|
45
|
+
declare const parseSiwdChallenge: (octets: Uint8Array) => SiwdChallenge;
|
|
46
|
+
interface CreateChallengeInput {
|
|
47
|
+
domain: string;
|
|
48
|
+
statement?: string;
|
|
49
|
+
did?: string;
|
|
50
|
+
/** Provide a nonce, or one is generated. */
|
|
51
|
+
nonce?: string;
|
|
52
|
+
/** Provide a timestamp, or `now` is used. */
|
|
53
|
+
timestamp?: string;
|
|
54
|
+
/** Clock injection (unix ms) for the timestamp. Default `Date.now()`. */
|
|
55
|
+
now?: () => number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Mint a challenge, clientless and edge-safe. Returns the challenge object, the
|
|
59
|
+
* base64url `encoded` form for the `/authorize?challenge=` query param, and the
|
|
60
|
+
* nonce for server-side replay storage. `encoded` is exactly
|
|
61
|
+
* base64url(siwdSigningInput(challenge)) — the same bytes the wallet signs.
|
|
62
|
+
*/
|
|
63
|
+
declare const createSiwdChallenge: (input: CreateChallengeInput) => {
|
|
64
|
+
challenge: SiwdChallenge;
|
|
65
|
+
encoded: string;
|
|
66
|
+
nonce: string;
|
|
67
|
+
};
|
|
68
|
+
/** Decode a base64url `challenge` query-param body back into a challenge object. */
|
|
69
|
+
declare const decodeSiwdChallenge: (encoded: string) => SiwdChallenge;
|
|
70
|
+
interface BuildSiwdSignRequestInput {
|
|
71
|
+
/** Requester DID that signs the courier envelope. */
|
|
72
|
+
did: string;
|
|
73
|
+
/** DID being asked to sign the SIWD artifact. */
|
|
74
|
+
subject: string;
|
|
75
|
+
challenge: SiwdChallenge;
|
|
76
|
+
/** Explicit composer policy; no implicit SIWD mailbox lifetime exists. */
|
|
77
|
+
acceptanceWindowSeconds: number;
|
|
78
|
+
expiresAt: string;
|
|
79
|
+
createdAt?: string;
|
|
80
|
+
signer: Signer;
|
|
81
|
+
keyId: string;
|
|
82
|
+
}
|
|
83
|
+
interface ValidateSiwdSignRequestOptions {
|
|
84
|
+
signerDid: string;
|
|
85
|
+
/** Must be the composer's stated acceptance policy for this request. */
|
|
86
|
+
acceptanceWindowSeconds: number;
|
|
87
|
+
resolveIdentity: (did: string) => Promise<VerifiedIdentity | undefined>;
|
|
88
|
+
/** Unix milliseconds; defaults to Date.now(). */
|
|
89
|
+
now?: number;
|
|
90
|
+
}
|
|
91
|
+
interface ValidatedSiwdSignRequest extends VerifiedSignRequest {
|
|
92
|
+
/** Strictly parsed canonical challenge used by the signer for rendering. */
|
|
93
|
+
challenge: SiwdChallenge;
|
|
94
|
+
}
|
|
95
|
+
/** Compose SIWD profile B using the generic sign-request envelope. */
|
|
96
|
+
declare const buildSiwdSignRequest: (input: BuildSiwdSignRequestInput) => Promise<{
|
|
97
|
+
jwsToken: string;
|
|
98
|
+
requestCID: string;
|
|
99
|
+
}>;
|
|
100
|
+
/** Verify and strictly parse a SIWD profile-B request before rendering/signing. */
|
|
101
|
+
declare const validateSiwdSignRequest: (jwsToken: string, options: ValidateSiwdSignRequestOptions) => Promise<ValidatedSiwdSignRequest>;
|
|
102
|
+
interface SiwdExpectations {
|
|
103
|
+
/** The verifier's own origin — MUST match the challenge domain. */
|
|
104
|
+
domain: string;
|
|
105
|
+
/** The nonce this verifier issued for the session. */
|
|
106
|
+
nonce: string;
|
|
107
|
+
/** If set, the challenge (and identity) MUST bind to this DID. */
|
|
108
|
+
did?: string;
|
|
109
|
+
/** If set, the signed challenge's timestamp MUST equal this exact value. */
|
|
110
|
+
timestamp?: string;
|
|
111
|
+
/** Reject challenges older than this many seconds. Default 300 (5 min). */
|
|
112
|
+
maxAgeSeconds?: number;
|
|
113
|
+
/**
|
|
114
|
+
* Accept an identity resolution whose tip could not be verified (cache-only
|
|
115
|
+
* or empty-delta-against-cache). Default FALSE: authentication never silently
|
|
116
|
+
* degrades — a rotated-out key must not verify against a stale cached state.
|
|
117
|
+
*/
|
|
118
|
+
allowStale?: boolean;
|
|
119
|
+
/** Clock injection (unix ms). Default `Date.now()`. */
|
|
120
|
+
now?: () => number;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Verify a SIWD challenge JWS. No-throw. The signature is checked under the DFOS
|
|
124
|
+
* profile (via the protocol's `verifyJws`), against a CURRENT `authKeys` entry
|
|
125
|
+
* of a non-deleted identity resolved through the client. Nonce, timestamp
|
|
126
|
+
* window, and domain are validated against `expect`.
|
|
127
|
+
*/
|
|
128
|
+
declare const verifySiwd: (client: Client, jws: string, expect: SiwdExpectations) => Promise<VerifyResult<SiwdSession>>;
|
|
129
|
+
|
|
130
|
+
export { type BuildSiwdSignRequestInput, type CreateChallengeInput, SIWD_JWS_TYP, type SiwdChallenge, type SiwdExpectations, type SiwdSession, type ValidateSiwdSignRequestOptions, type ValidatedSiwdSignRequest, buildSiwdSignRequest, createSiwdChallenge, decodeSiwdChallenge, parseSiwdChallenge, siwdSigningInput, validateSiwdSignRequest, verifySiwd };
|
package/dist/siwd.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// src/siwd.ts
|
|
2
|
+
import {
|
|
3
|
+
buildSignRequest,
|
|
4
|
+
decodeMultikey,
|
|
5
|
+
SignRequestVerifyError,
|
|
6
|
+
verifySignRequest
|
|
7
|
+
} from "@metalabel/dfos-protocol/chain";
|
|
8
|
+
import {
|
|
9
|
+
assertJwsProfile,
|
|
10
|
+
base64urlDecode,
|
|
11
|
+
base64urlEncode,
|
|
12
|
+
decodeJwsUnsafe,
|
|
13
|
+
generateIdNoPrefix,
|
|
14
|
+
verifyJws
|
|
15
|
+
} from "@metalabel/dfos-protocol/crypto";
|
|
16
|
+
var SIWD_JWS_TYP = "did:dfos:siwd";
|
|
17
|
+
var MAX_CLOCK_SKEW_SECONDS = 60;
|
|
18
|
+
var SIWD_CHALLENGE_FIELDS = /* @__PURE__ */ new Set(["domain", "nonce", "timestamp", "statement", "did"]);
|
|
19
|
+
var WHOLE_SECOND_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z$/;
|
|
20
|
+
var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
21
|
+
var MAX_SIGN_REQUEST_WINDOW_SECONDS = 604800;
|
|
22
|
+
var validateSiwdChallenge = (value) => {
|
|
23
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
24
|
+
throw new Error("invalid SIWD challenge: expected a JSON object");
|
|
25
|
+
}
|
|
26
|
+
const raw = value;
|
|
27
|
+
for (const field of Object.keys(raw)) {
|
|
28
|
+
if (!SIWD_CHALLENGE_FIELDS.has(field)) {
|
|
29
|
+
throw new Error(`invalid SIWD challenge: unknown member ${field}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const field of ["domain", "nonce", "timestamp"]) {
|
|
33
|
+
if (typeof raw[field] !== "string" || raw[field].length === 0) {
|
|
34
|
+
throw new Error(`invalid SIWD challenge: ${field} must be a non-empty string`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const field of ["statement", "did"]) {
|
|
38
|
+
if (raw[field] !== void 0 && typeof raw[field] !== "string") {
|
|
39
|
+
throw new Error(`invalid SIWD challenge: ${field} must be a string when present`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const strings = [raw["domain"], raw["nonce"], raw["timestamp"], raw["statement"], raw["did"]];
|
|
43
|
+
if (strings.some((field) => typeof field === "string" && LONE_SURROGATE.test(field))) {
|
|
44
|
+
throw new Error("invalid SIWD challenge: string members must be well-formed Unicode");
|
|
45
|
+
}
|
|
46
|
+
const timestamp = raw["timestamp"];
|
|
47
|
+
const timestampMs = Date.parse(timestamp);
|
|
48
|
+
if (!WHOLE_SECOND_TIMESTAMP.test(timestamp) || Number.isNaN(timestampMs) || new Date(timestampMs).toISOString() !== timestamp) {
|
|
49
|
+
throw new Error("invalid SIWD challenge: timestamp must be ISO-8601 UTC whole-second .000Z");
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
domain: raw["domain"],
|
|
53
|
+
nonce: raw["nonce"],
|
|
54
|
+
timestamp,
|
|
55
|
+
...raw["statement"] !== void 0 ? { statement: raw["statement"] } : {},
|
|
56
|
+
...raw["did"] !== void 0 ? { did: raw["did"] } : {}
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
var siwdSigningInput = (challenge) => {
|
|
60
|
+
const parsed = validateSiwdChallenge(challenge);
|
|
61
|
+
const canonical = {
|
|
62
|
+
domain: parsed.domain,
|
|
63
|
+
nonce: parsed.nonce,
|
|
64
|
+
timestamp: parsed.timestamp
|
|
65
|
+
};
|
|
66
|
+
if (parsed.statement !== void 0) canonical["statement"] = parsed.statement;
|
|
67
|
+
if (parsed.did !== void 0) canonical["did"] = parsed.did;
|
|
68
|
+
return new TextEncoder().encode(JSON.stringify(canonical));
|
|
69
|
+
};
|
|
70
|
+
var parseSiwdChallenge = (octets) => {
|
|
71
|
+
let source;
|
|
72
|
+
try {
|
|
73
|
+
source = new TextDecoder("utf-8", { fatal: true }).decode(octets);
|
|
74
|
+
} catch {
|
|
75
|
+
throw new Error("invalid SIWD challenge: payload is not valid UTF-8 JSON");
|
|
76
|
+
}
|
|
77
|
+
if (source.startsWith("\uFEFF")) {
|
|
78
|
+
throw new Error("invalid SIWD challenge: payload must not carry a UTF-8 BOM");
|
|
79
|
+
}
|
|
80
|
+
let raw;
|
|
81
|
+
try {
|
|
82
|
+
raw = JSON.parse(source);
|
|
83
|
+
} catch {
|
|
84
|
+
throw new Error("invalid SIWD challenge: payload is not valid JSON");
|
|
85
|
+
}
|
|
86
|
+
const challenge = validateSiwdChallenge(raw);
|
|
87
|
+
const canonical = siwdSigningInput(challenge);
|
|
88
|
+
if (canonical.length !== octets.length || !canonical.every((byte, index) => byte === octets[index])) {
|
|
89
|
+
throw new Error("invalid SIWD challenge: payload bytes are not canonical");
|
|
90
|
+
}
|
|
91
|
+
return challenge;
|
|
92
|
+
};
|
|
93
|
+
var createSiwdChallenge = (input) => {
|
|
94
|
+
const nonce = input.nonce ?? generateIdNoPrefix();
|
|
95
|
+
const timestampValue = input.timestamp ?? new Date(input.now ? input.now() : Date.now()).toISOString();
|
|
96
|
+
const timestampMs = Date.parse(timestampValue);
|
|
97
|
+
if (Number.isNaN(timestampMs)) {
|
|
98
|
+
throw new Error(`invalid SIWD challenge: unparseable timestamp: ${timestampValue}`);
|
|
99
|
+
}
|
|
100
|
+
const timestamp = new Date(Math.floor(timestampMs / 1e3) * 1e3).toISOString();
|
|
101
|
+
const challenge = {
|
|
102
|
+
domain: input.domain,
|
|
103
|
+
nonce,
|
|
104
|
+
timestamp,
|
|
105
|
+
...input.statement !== void 0 ? { statement: input.statement } : {},
|
|
106
|
+
...input.did !== void 0 ? { did: input.did } : {}
|
|
107
|
+
};
|
|
108
|
+
const encoded = base64urlEncode(siwdSigningInput(challenge));
|
|
109
|
+
return { challenge, encoded, nonce };
|
|
110
|
+
};
|
|
111
|
+
var decodeSiwdChallenge = (encoded) => parseSiwdChallenge(base64urlDecode(encoded));
|
|
112
|
+
var assertSiwdAcceptanceWindow = (seconds) => {
|
|
113
|
+
if (!Number.isSafeInteger(seconds) || seconds <= 0) {
|
|
114
|
+
throw new Error("SIWD acceptanceWindowSeconds must be a positive integer");
|
|
115
|
+
}
|
|
116
|
+
if (seconds > MAX_SIGN_REQUEST_WINDOW_SECONDS) {
|
|
117
|
+
throw new Error("SIWD acceptanceWindowSeconds exceeds the sign-request 604800-second ceiling");
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
var assertSiwdOneClock = (challengeTimestamp, expiresAt, acceptanceWindowSeconds) => {
|
|
121
|
+
assertSiwdAcceptanceWindow(acceptanceWindowSeconds);
|
|
122
|
+
const challengeMs = Date.parse(challengeTimestamp);
|
|
123
|
+
const expiresMs = Date.parse(expiresAt);
|
|
124
|
+
if (Number.isNaN(expiresMs)) {
|
|
125
|
+
throw new Error(`invalid SIWD sign request: unparseable expiresAt: ${expiresAt}`);
|
|
126
|
+
}
|
|
127
|
+
const normalizedExpiresMs = Math.floor(expiresMs / 1e3) * 1e3;
|
|
128
|
+
if (normalizedExpiresMs > challengeMs + acceptanceWindowSeconds * 1e3) {
|
|
129
|
+
throw new Error("SIWD sign request expiresAt exceeds the challenge acceptance window");
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
var buildSiwdSignRequest = async (input) => {
|
|
133
|
+
const challenge = validateSiwdChallenge(input.challenge);
|
|
134
|
+
if (challenge.did !== void 0 && challenge.did !== input.subject) {
|
|
135
|
+
throw new Error("SIWD challenge did does not match sign request subject");
|
|
136
|
+
}
|
|
137
|
+
assertSiwdOneClock(challenge.timestamp, input.expiresAt, input.acceptanceWindowSeconds);
|
|
138
|
+
return buildSignRequest({
|
|
139
|
+
did: input.did,
|
|
140
|
+
subject: input.subject,
|
|
141
|
+
payloadTyp: SIWD_JWS_TYP,
|
|
142
|
+
payload: siwdSigningInput(challenge),
|
|
143
|
+
expiresAt: input.expiresAt,
|
|
144
|
+
...input.createdAt !== void 0 ? { createdAt: input.createdAt } : {},
|
|
145
|
+
signer: input.signer,
|
|
146
|
+
keyId: input.keyId
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
var validateSiwdSignRequest = async (jwsToken, options) => {
|
|
150
|
+
assertSiwdAcceptanceWindow(options.acceptanceWindowSeconds);
|
|
151
|
+
const request = await verifySignRequest(jwsToken, {
|
|
152
|
+
resolveIdentity: options.resolveIdentity,
|
|
153
|
+
...options.now !== void 0 ? { now: options.now } : {}
|
|
154
|
+
});
|
|
155
|
+
const invalid = (message) => new SignRequestVerifyError("invalid", message);
|
|
156
|
+
if (request.subject !== options.signerDid) {
|
|
157
|
+
throw invalid("SIWD sign request subject does not match signer DID");
|
|
158
|
+
}
|
|
159
|
+
if (request.payloadTyp !== SIWD_JWS_TYP) {
|
|
160
|
+
throw invalid(`invalid SIWD sign request payloadTyp: ${request.payloadTyp}`);
|
|
161
|
+
}
|
|
162
|
+
let challenge;
|
|
163
|
+
try {
|
|
164
|
+
challenge = parseSiwdChallenge(request.payloadBytes);
|
|
165
|
+
assertSiwdOneClock(challenge.timestamp, request.expiresAt, options.acceptanceWindowSeconds);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
throw invalid(err instanceof Error ? err.message : "invalid SIWD sign request payload");
|
|
168
|
+
}
|
|
169
|
+
if (challenge.did !== void 0 && challenge.did !== options.signerDid) {
|
|
170
|
+
throw invalid("SIWD challenge did does not match signer DID");
|
|
171
|
+
}
|
|
172
|
+
return { ...request, challenge };
|
|
173
|
+
};
|
|
174
|
+
var fail = (error) => ({ ok: false, error });
|
|
175
|
+
var verifySiwd = async (client, jws, expect) => {
|
|
176
|
+
try {
|
|
177
|
+
const decoded = decodeJwsUnsafe(jws);
|
|
178
|
+
if (!decoded) return fail("failed to decode JWS");
|
|
179
|
+
const rawHeader = decoded.header;
|
|
180
|
+
if (typeof rawHeader !== "object" || rawHeader === null || Array.isArray(rawHeader)) {
|
|
181
|
+
return fail("SIWD protected header must be an object");
|
|
182
|
+
}
|
|
183
|
+
assertJwsProfile(rawHeader, (message) => new Error(message));
|
|
184
|
+
if (decoded.header.typ !== SIWD_JWS_TYP) {
|
|
185
|
+
return fail(`invalid typ: expected ${SIWD_JWS_TYP}, got ${decoded.header.typ}`);
|
|
186
|
+
}
|
|
187
|
+
const kid = decoded.header.kid;
|
|
188
|
+
if (typeof kid !== "string") return fail("kid must be a DID URL");
|
|
189
|
+
const hashIdx = kid.indexOf("#");
|
|
190
|
+
if (hashIdx < 0) return fail("kid must be a DID URL");
|
|
191
|
+
const did = kid.substring(0, hashIdx);
|
|
192
|
+
const keyId = kid.substring(hashIdx + 1);
|
|
193
|
+
const payloadSegment = jws.split(".")[1];
|
|
194
|
+
if (payloadSegment === void 0) return fail("failed to decode JWS payload");
|
|
195
|
+
const payload = parseSiwdChallenge(base64urlDecode(payloadSegment));
|
|
196
|
+
if (payload.did !== void 0 && payload.did !== did) {
|
|
197
|
+
return fail("challenge did does not match signer");
|
|
198
|
+
}
|
|
199
|
+
if (expect.did !== void 0 && expect.did !== did) {
|
|
200
|
+
return fail("signer did does not match expected did");
|
|
201
|
+
}
|
|
202
|
+
const resolved = await client.identity(did);
|
|
203
|
+
const unverifiable = resolved.trust.unverifiable ?? [];
|
|
204
|
+
if (!expect.allowStale && (unverifiable.includes("tip") || resolved.provenance.fromCache)) {
|
|
205
|
+
return fail(
|
|
206
|
+
"identity resolution is stale (tip unverified) \u2014 refusing to authenticate against a cached identity state; pass allowStale: true to accept the risk"
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const state = resolved.value;
|
|
210
|
+
if (state.isDeleted) return fail("identity is deleted");
|
|
211
|
+
const authKey = state.authKeys.find((k) => k.id === keyId);
|
|
212
|
+
if (!authKey) return fail("signing key is not a current authentication key");
|
|
213
|
+
const publicKey = decodeMultikey(authKey.publicKeyMultibase).keyBytes;
|
|
214
|
+
try {
|
|
215
|
+
verifyJws({ token: jws, publicKey });
|
|
216
|
+
} catch (err) {
|
|
217
|
+
return fail(err instanceof Error ? err.message : "invalid signature");
|
|
218
|
+
}
|
|
219
|
+
if (payload.nonce !== expect.nonce) return fail("nonce mismatch");
|
|
220
|
+
if (payload.domain !== expect.domain) return fail("domain mismatch");
|
|
221
|
+
if (expect.timestamp !== void 0 && payload.timestamp !== expect.timestamp) {
|
|
222
|
+
return fail("timestamp does not match expected challenge timestamp");
|
|
223
|
+
}
|
|
224
|
+
const maxAge = expect.maxAgeSeconds ?? 300;
|
|
225
|
+
const nowMs = expect.now ? expect.now() : Date.now();
|
|
226
|
+
const issuedMs = Date.parse(payload.timestamp);
|
|
227
|
+
if (Number.isNaN(issuedMs)) return fail("invalid timestamp");
|
|
228
|
+
if (nowMs - issuedMs > maxAge * 1e3) return fail("challenge expired");
|
|
229
|
+
if (issuedMs - nowMs > MAX_CLOCK_SKEW_SECONDS * 1e3) {
|
|
230
|
+
return fail("challenge timestamp is in the future");
|
|
231
|
+
}
|
|
232
|
+
const session = {
|
|
233
|
+
did,
|
|
234
|
+
domain: payload.domain,
|
|
235
|
+
nonce: payload.nonce,
|
|
236
|
+
timestamp: payload.timestamp,
|
|
237
|
+
kid,
|
|
238
|
+
...payload.statement !== void 0 ? { statement: payload.statement } : {}
|
|
239
|
+
};
|
|
240
|
+
return {
|
|
241
|
+
ok: true,
|
|
242
|
+
value: session,
|
|
243
|
+
// surface the resolution's honest gaps either way (populated only on the
|
|
244
|
+
// allowStale path — the default path fails closed above)
|
|
245
|
+
...unverifiable.length > 0 ? { unverifiable } : {}
|
|
246
|
+
};
|
|
247
|
+
} catch (err) {
|
|
248
|
+
return fail(err instanceof Error ? err.message : "siwd verification failed");
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
export {
|
|
252
|
+
SIWD_JWS_TYP,
|
|
253
|
+
buildSiwdSignRequest,
|
|
254
|
+
createSiwdChallenge,
|
|
255
|
+
decodeSiwdChallenge,
|
|
256
|
+
parseSiwdChallenge,
|
|
257
|
+
siwdSigningInput,
|
|
258
|
+
validateSiwdSignRequest,
|
|
259
|
+
verifySiwd
|
|
260
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { m as memoryStore } from '../memory-CL1DM6Ud.js';
|
|
2
|
+
import { S as Store } from '../types-ByxTj1u-.js';
|
|
3
|
+
import '@metalabel/dfos-protocol/chain';
|
|
4
|
+
import '@metalabel/dfos-protocol/credentials';
|
|
5
|
+
import '@metalabel/dfos-web-relay/peer-client';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A durable, browser-only cache backed by a single IndexedDB object store.
|
|
9
|
+
* `dbName` defaults to `dfos-client`; pass a distinct name to isolate caches
|
|
10
|
+
* (e.g. per relay set) since a relay switch is a new client.
|
|
11
|
+
*/
|
|
12
|
+
declare const indexedDbStore: (dbName?: string) => Store;
|
|
13
|
+
|
|
14
|
+
export { Store, indexedDbStore };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
memoryStore
|
|
3
|
+
} from "../chunk-63XCFYOE.js";
|
|
4
|
+
|
|
5
|
+
// src/store/indexeddb.ts
|
|
6
|
+
var STORE_NAME = "dfos-cache";
|
|
7
|
+
var getFactory = () => {
|
|
8
|
+
const idb = globalThis.indexedDB;
|
|
9
|
+
if (!idb) throw new Error("indexedDbStore requires a browser IndexedDB environment");
|
|
10
|
+
return idb;
|
|
11
|
+
};
|
|
12
|
+
var promisify = (req) => new Promise((resolve, reject) => {
|
|
13
|
+
req.onsuccess = () => resolve(req.result);
|
|
14
|
+
req.onerror = () => reject(req.error ?? new Error("indexeddb request failed"));
|
|
15
|
+
});
|
|
16
|
+
var indexedDbStore = (dbName = "dfos-client") => {
|
|
17
|
+
let dbPromise;
|
|
18
|
+
const open = () => {
|
|
19
|
+
if (!dbPromise) {
|
|
20
|
+
dbPromise = new Promise((resolve, reject) => {
|
|
21
|
+
const req = getFactory().open(dbName, 1);
|
|
22
|
+
req.onupgradeneeded = () => {
|
|
23
|
+
const db = req.result;
|
|
24
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) db.createObjectStore(STORE_NAME);
|
|
25
|
+
};
|
|
26
|
+
req.onsuccess = () => resolve(req.result);
|
|
27
|
+
req.onerror = () => reject(req.error ?? new Error("failed to open indexeddb"));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return dbPromise;
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
async get(key) {
|
|
34
|
+
const db = await open();
|
|
35
|
+
const store = db.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME);
|
|
36
|
+
const value = await promisify(store.get(key));
|
|
37
|
+
return value ?? void 0;
|
|
38
|
+
},
|
|
39
|
+
async set(key, value) {
|
|
40
|
+
const db = await open();
|
|
41
|
+
const store = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME);
|
|
42
|
+
await promisify(store.put(value, key));
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
export {
|
|
47
|
+
indexedDbStore,
|
|
48
|
+
memoryStore
|
|
49
|
+
};
|