@witnium-tech/witniumchain 0.3.0 → 0.6.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/FRONTEND-INTEGRATION.md +265 -0
- package/README.md +95 -19
- package/dist/index.d.mts +4439 -62
- package/dist/index.d.ts +4439 -62
- package/dist/index.js +812 -28
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +808 -29
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -7
- package/README.md.bak +0 -160
package/dist/index.mjs
CHANGED
|
@@ -12,12 +12,103 @@ var WitniumchainApiError = class extends Error {
|
|
|
12
12
|
}
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
// src/pkce.ts
|
|
16
|
+
var STORAGE_PREFIX = "witniumchain.pkce.";
|
|
17
|
+
function defaultVerifierStorage() {
|
|
18
|
+
const storage = globalThis.sessionStorage;
|
|
19
|
+
if (!storage) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
"WitniumchainClient: defaultVerifierStorage requires globalThis.sessionStorage. In a non-browser context, pass `verifierStorage` to the OAuth helpers."
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
set(stateKey, verifier) {
|
|
26
|
+
storage.setItem(STORAGE_PREFIX + stateKey, verifier);
|
|
27
|
+
},
|
|
28
|
+
get(stateKey) {
|
|
29
|
+
return storage.getItem(STORAGE_PREFIX + stateKey);
|
|
30
|
+
},
|
|
31
|
+
remove(stateKey) {
|
|
32
|
+
storage.removeItem(STORAGE_PREFIX + stateKey);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function generateCodeVerifier() {
|
|
37
|
+
const bytes = new Uint8Array(64);
|
|
38
|
+
cryptoRef().getRandomValues(bytes);
|
|
39
|
+
return base64UrlEncode(bytes);
|
|
40
|
+
}
|
|
41
|
+
async function deriveCodeChallenge(verifier) {
|
|
42
|
+
const data = new TextEncoder().encode(verifier);
|
|
43
|
+
const digest = await cryptoRef().subtle.digest("SHA-256", data);
|
|
44
|
+
return base64UrlEncode(new Uint8Array(digest));
|
|
45
|
+
}
|
|
46
|
+
function generateState() {
|
|
47
|
+
const bytes = new Uint8Array(32);
|
|
48
|
+
cryptoRef().getRandomValues(bytes);
|
|
49
|
+
return base64UrlEncode(bytes);
|
|
50
|
+
}
|
|
51
|
+
function base64UrlEncode(bytes) {
|
|
52
|
+
let binary = "";
|
|
53
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
54
|
+
const b64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(binary, "binary").toString("base64");
|
|
55
|
+
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
56
|
+
}
|
|
57
|
+
function cryptoRef() {
|
|
58
|
+
const c = globalThis.crypto;
|
|
59
|
+
if (!c || !c.subtle || !c.getRandomValues) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"WitniumchainClient: globalThis.crypto with subtle + getRandomValues is required for PKCE. Modern browsers and Node 18+ provide this natively."
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return c;
|
|
65
|
+
}
|
|
66
|
+
|
|
15
67
|
// src/client.ts
|
|
16
68
|
var WitniumchainClient = class {
|
|
17
69
|
baseUrl;
|
|
70
|
+
chainBaseUrl;
|
|
18
71
|
cfg;
|
|
19
72
|
timeout;
|
|
20
73
|
fetchImpl;
|
|
74
|
+
// Mutable OAuth state. Constructor seeds these from the config; the OAuth
|
|
75
|
+
// flow helpers (begin/complete/refresh/signOut) read and rewrite them as
|
|
76
|
+
// the user authenticates and tokens rotate. `applyAuth` reads `accessToken`
|
|
77
|
+
// off this field — never directly off `cfg` — so a token rotated mid-flight
|
|
78
|
+
// (during a 401-retry refresh) is picked up by the very next call.
|
|
79
|
+
accessToken;
|
|
80
|
+
// Browser SPAs in production receive the refresh token as an HttpOnly cookie
|
|
81
|
+
// (see src/oauth/refresh-cookie.ts on the server), so this field stays
|
|
82
|
+
// `undefined` and the cookie rides via `credentials: 'include'` on every
|
|
83
|
+
// /token call. Non-browser callers (Node SSR, native apps without a cookie
|
|
84
|
+
// jar) get the refresh token in the response body and the SDK stashes it
|
|
85
|
+
// here as a fallback. Either path drives `refreshAccessToken` identically.
|
|
86
|
+
refreshToken;
|
|
87
|
+
// Sentinel: the most recent token response set an HttpOnly refresh cookie.
|
|
88
|
+
// The SDK can't directly observe an HttpOnly cookie, but the response body
|
|
89
|
+
// tells us indirectly — server strips `refresh_token` when it sets the
|
|
90
|
+
// cookie, so an absent body field on a successful /token response means
|
|
91
|
+
// the cookie path is in use. Used to gate the 401-retry: with a cookie,
|
|
92
|
+
// refresh might work even when the in-memory refresh token is undefined.
|
|
93
|
+
hasRefreshCookie = false;
|
|
94
|
+
oauthClientId;
|
|
95
|
+
// The redirect URI the most recent `beginOAuthLogin` issued, alongside the
|
|
96
|
+
// PKCE verifier. `completeOAuthLogin` reads it back so the token-exchange
|
|
97
|
+
// request matches the original /auth request (RFC 6749 §4.1.3 requires
|
|
98
|
+
// redirect_uri at /token to equal the one at /auth). Keyed by state.
|
|
99
|
+
pendingLogins = /* @__PURE__ */ new Map();
|
|
100
|
+
verifierStorage;
|
|
101
|
+
// Cache of the parsed OIDC discovery document keyed by issuer URL. Saves a
|
|
102
|
+
// round trip on every OAuth call after the first. oidc-provider's discovery
|
|
103
|
+
// doc is static for the life of an issuer; cache is per-client-instance, so
|
|
104
|
+
// it dies with the SDK consumer's lifecycle.
|
|
105
|
+
discoveryCache;
|
|
106
|
+
// Single-flight gate for refresh. When a 401 fans out to N concurrent retries
|
|
107
|
+
// (or the consumer calls refreshAccessToken directly while another refresh
|
|
108
|
+
// is mid-flight), all callers await the same in-flight promise — refresh
|
|
109
|
+
// tokens rotate on use (D10), so a second concurrent refresh would race the
|
|
110
|
+
// first and one of them would 401.
|
|
111
|
+
refreshInFlight;
|
|
21
112
|
/** Subscriptions / billing helpers. See {@link Subscriptions}. */
|
|
22
113
|
subscriptions;
|
|
23
114
|
/** Delegated-key namespace including the one-call {@link DelegatedKeys.provision} flow. */
|
|
@@ -26,12 +117,15 @@ var WitniumchainClient = class {
|
|
|
26
117
|
keys;
|
|
27
118
|
/** OAuth session management. Accessed as `client.oauth.sessions.*`. */
|
|
28
119
|
oauth;
|
|
120
|
+
/** MFA self-management. Accessed as `client.mfa.totp.*` and `client.mfa.recoveryCodes.*`. */
|
|
121
|
+
mfa;
|
|
29
122
|
constructor(config) {
|
|
30
123
|
if (!config.baseUrl) {
|
|
31
124
|
throw new Error("WitniumchainClient: baseUrl is required");
|
|
32
125
|
}
|
|
33
126
|
this.cfg = config;
|
|
34
127
|
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
128
|
+
this.chainBaseUrl = config.chainBaseUrl?.replace(/\/$/, "");
|
|
35
129
|
this.timeout = config.timeout ?? 3e4;
|
|
36
130
|
this.fetchImpl = config.fetch ?? globalThis.fetch;
|
|
37
131
|
if (!this.fetchImpl) {
|
|
@@ -39,10 +133,14 @@ var WitniumchainClient = class {
|
|
|
39
133
|
"WitniumchainClient: no fetch implementation available. Pass `config.fetch`."
|
|
40
134
|
);
|
|
41
135
|
}
|
|
136
|
+
this.accessToken = config.accessToken;
|
|
137
|
+
this.oauthClientId = config.oauthClientId;
|
|
138
|
+
this.verifierStorage = config.verifierStorage;
|
|
42
139
|
this.subscriptions = new Subscriptions(this);
|
|
43
140
|
this.delegatedKeys = new DelegatedKeys(this);
|
|
44
141
|
this.keys = new SigningKeys(this);
|
|
45
142
|
this.oauth = new OauthNamespace(this);
|
|
143
|
+
this.mfa = new MfaNamespace(this);
|
|
46
144
|
}
|
|
47
145
|
/**
|
|
48
146
|
* Convenience alias for {@link getAccount} — returns the authenticated
|
|
@@ -57,6 +155,19 @@ var WitniumchainClient = class {
|
|
|
57
155
|
signup(body) {
|
|
58
156
|
return this.req("POST", "/v1/auth/signup", { auth: "Public", body });
|
|
59
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Self-serve org creation (Phase RBAC Thread D). One public call that
|
|
160
|
+
* creates the admin user, the org, the org-admin membership, and deploys
|
|
161
|
+
* the contract with the caller's client-generated `ownerPublicKey` +
|
|
162
|
+
* `initialSigningPublicKey`. Witnium never sees the private bytes.
|
|
163
|
+
*
|
|
164
|
+
* Returns `{ orgId, userId, contractAddress, contractVersion,
|
|
165
|
+
* emailVerifyToken }`. The verify token is also emailed; it's echoed here
|
|
166
|
+
* for UIs that prefer to render the verify link themselves.
|
|
167
|
+
*/
|
|
168
|
+
createOrg(body) {
|
|
169
|
+
return this.req("POST", "/v1/orgs", { auth: "Public", body });
|
|
170
|
+
}
|
|
60
171
|
verifyEmail(token) {
|
|
61
172
|
return this.req("GET", "/v1/auth/verify", {
|
|
62
173
|
auth: "Public",
|
|
@@ -210,8 +321,8 @@ var WitniumchainClient = class {
|
|
|
210
321
|
// ────────────────────────────────────────────────────────────────────────
|
|
211
322
|
// Witnesses (/v1/contracts/{addr}/witnesses/*)
|
|
212
323
|
// ────────────────────────────────────────────────────────────────────────
|
|
213
|
-
proposeWitness(contractAddress, body, idempotencyKey) {
|
|
214
|
-
const key = idempotencyKey ??
|
|
324
|
+
async proposeWitness(contractAddress, body, idempotencyKey) {
|
|
325
|
+
const key = idempotencyKey ?? await deriveBodyKey("v1:propose", contractAddress, body);
|
|
215
326
|
return this.req(
|
|
216
327
|
"POST",
|
|
217
328
|
`/v1/contracts/${encodeURIComponent(contractAddress)}/witnesses/propose`,
|
|
@@ -232,8 +343,8 @@ var WitniumchainClient = class {
|
|
|
232
343
|
{ auth: "SignedRequest" }
|
|
233
344
|
);
|
|
234
345
|
}
|
|
235
|
-
revokeWitness(contractAddress, witnessId, body, idempotencyKey) {
|
|
236
|
-
const key = idempotencyKey ??
|
|
346
|
+
async revokeWitness(contractAddress, witnessId, body, idempotencyKey) {
|
|
347
|
+
const key = idempotencyKey ?? await deriveBodyKey("v1:revoke", contractAddress, { witnessId, body });
|
|
237
348
|
return this.req(
|
|
238
349
|
"POST",
|
|
239
350
|
`/v1/contracts/${encodeURIComponent(contractAddress)}/witnesses/${encodeURIComponent(witnessId)}/revoke`,
|
|
@@ -255,12 +366,15 @@ var WitniumchainClient = class {
|
|
|
255
366
|
// chain-api with the admin token. Auth is OAuth Bearer; the URL
|
|
256
367
|
// contract must match the user's bound contract.
|
|
257
368
|
//
|
|
258
|
-
// The propose/revoke methods
|
|
259
|
-
//
|
|
260
|
-
//
|
|
369
|
+
// The propose/revoke methods default Idempotency-Key to a stable hash
|
|
370
|
+
// of the request body so an application-level retry with the same
|
|
371
|
+
// arguments dedupes against the original reservation instead of
|
|
372
|
+
// burning a second credit. Pass an explicit `idempotencyKey` to
|
|
373
|
+
// override (e.g. if you genuinely want two witnesses for the same
|
|
374
|
+
// body).
|
|
261
375
|
// ────────────────────────────────────────────────────────────────────────
|
|
262
|
-
proposeWitnessV5(contractAddress, body, idempotencyKey) {
|
|
263
|
-
const key = idempotencyKey ??
|
|
376
|
+
async proposeWitnessV5(contractAddress, body, idempotencyKey) {
|
|
377
|
+
const key = idempotencyKey ?? await deriveBodyKey("v5:propose", contractAddress, body);
|
|
264
378
|
return this.req(
|
|
265
379
|
"POST",
|
|
266
380
|
`/v5/contracts/${encodeURIComponent(contractAddress)}/witnesses/propose`,
|
|
@@ -281,8 +395,8 @@ var WitniumchainClient = class {
|
|
|
281
395
|
{ auth: "BearerJWT" }
|
|
282
396
|
);
|
|
283
397
|
}
|
|
284
|
-
revokeWitnessV5(contractAddress, witnessId, body, idempotencyKey) {
|
|
285
|
-
const key = idempotencyKey ??
|
|
398
|
+
async revokeWitnessV5(contractAddress, witnessId, body, idempotencyKey) {
|
|
399
|
+
const key = idempotencyKey ?? await deriveBodyKey("v5:revoke", contractAddress, { witnessId, body });
|
|
286
400
|
return this.req(
|
|
287
401
|
"POST",
|
|
288
402
|
`/v5/contracts/${encodeURIComponent(contractAddress)}/witnesses/${encodeURIComponent(witnessId)}/revoke`,
|
|
@@ -299,6 +413,37 @@ var WitniumchainClient = class {
|
|
|
299
413
|
return this.req("GET", "/v1/account/ledger", { auth: "SessionCookie" });
|
|
300
414
|
}
|
|
301
415
|
// ────────────────────────────────────────────────────────────────────────
|
|
416
|
+
// MFA (/v1/account/mfa/*)
|
|
417
|
+
//
|
|
418
|
+
// SessionCookie auth (self-management). Returns the same shapes the
|
|
419
|
+
// dashboard renders directly; the SDK is also a viable consumer for any
|
|
420
|
+
// Node-side tooling that needs to programmatically enrol a service account.
|
|
421
|
+
//
|
|
422
|
+
// The MFA challenge step that runs inside the OAuth interaction (Thread E)
|
|
423
|
+
// is NOT here — it's a server-rendered HTML form, not a JSON surface.
|
|
424
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
425
|
+
enrollTotp() {
|
|
426
|
+
return this.req("POST", "/v1/account/mfa/totp/enroll", {
|
|
427
|
+
auth: "SessionCookie"
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
confirmTotp(body) {
|
|
431
|
+
return this.req("POST", "/v1/account/mfa/totp/confirm", {
|
|
432
|
+
auth: "SessionCookie",
|
|
433
|
+
body
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
disableTotp() {
|
|
437
|
+
return this.req("DELETE", "/v1/account/mfa/totp", {
|
|
438
|
+
auth: "SessionCookie"
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
regenerateRecoveryCodes() {
|
|
442
|
+
return this.req("POST", "/v1/account/mfa/recovery-codes/regenerate", {
|
|
443
|
+
auth: "SessionCookie"
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
302
447
|
// OAuth sessions (/v1/oauth/sessions*)
|
|
303
448
|
// ────────────────────────────────────────────────────────────────────────
|
|
304
449
|
listOauthSessions() {
|
|
@@ -325,12 +470,408 @@ var WitniumchainClient = class {
|
|
|
325
470
|
healthReady() {
|
|
326
471
|
return this.req("GET", "/health/ready", { auth: "Public" });
|
|
327
472
|
}
|
|
473
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
474
|
+
// Chain-api reads (called against chainBaseUrl, e.g. api.witniumchain.com)
|
|
475
|
+
//
|
|
476
|
+
// Routing: every method here passes `service: 'chain'` to `req()`. Calls
|
|
477
|
+
// throw at call time if `chainBaseUrl` wasn't configured.
|
|
478
|
+
//
|
|
479
|
+
// Auth: BearerJWT. The accounts-issued OAuth access token already carries
|
|
480
|
+
// `aud=https://api.witniumchain.com`, so the same `accessToken` works
|
|
481
|
+
// unchanged against both services.
|
|
482
|
+
//
|
|
483
|
+
// What's NOT here: chain-api v5 WRITES (propose/sign/finalize/revoke). Those
|
|
484
|
+
// are proxied by accounts (proposeWitnessV5, etc.) so credits get reserved
|
|
485
|
+
// and idempotency is enforced. See docs/PLAN-PHASE-SDK-UNIFIED.md for the
|
|
486
|
+
// routing rules; calling chain-api writes directly would burn credits
|
|
487
|
+
// without billing them.
|
|
488
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
489
|
+
getContractInfo(contractAddress) {
|
|
490
|
+
return this.req(
|
|
491
|
+
"GET",
|
|
492
|
+
`/v5/contracts/${encodeURIComponent(contractAddress)}/info`,
|
|
493
|
+
{ auth: "BearerJWT", service: "chain" }
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
getContractVerification(contractAddress) {
|
|
497
|
+
return this.req(
|
|
498
|
+
"GET",
|
|
499
|
+
`/v5/contracts/${encodeURIComponent(contractAddress)}/verify`,
|
|
500
|
+
{ auth: "BearerJWT", service: "chain" }
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
listWitnessesV5(contractAddress, params) {
|
|
504
|
+
return this.req(
|
|
505
|
+
"GET",
|
|
506
|
+
`/v5/contracts/${encodeURIComponent(contractAddress)}/witnesses`,
|
|
507
|
+
{ auth: "BearerJWT", service: "chain", query: params }
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
getWitnessV5(contractAddress, witnessId) {
|
|
511
|
+
return this.req(
|
|
512
|
+
"GET",
|
|
513
|
+
`/v5/contracts/${encodeURIComponent(contractAddress)}/witnesses/${encodeURIComponent(witnessId)}`,
|
|
514
|
+
{ auth: "BearerJWT", service: "chain" }
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
getContractTransaction(contractAddress, txHash) {
|
|
518
|
+
return this.req(
|
|
519
|
+
"GET",
|
|
520
|
+
`/v5/contracts/${encodeURIComponent(contractAddress)}/transactions/${encodeURIComponent(txHash)}`,
|
|
521
|
+
{ auth: "BearerJWT", service: "chain" }
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
getTransaction(txHash) {
|
|
525
|
+
return this.req(
|
|
526
|
+
"GET",
|
|
527
|
+
`/v5/transactions/${encodeURIComponent(txHash)}`,
|
|
528
|
+
{ auth: "BearerJWT", service: "chain" }
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
getWalletBalance(address) {
|
|
532
|
+
return this.req(
|
|
533
|
+
"GET",
|
|
534
|
+
`/v5/wallets/${encodeURIComponent(address)}/balance`,
|
|
535
|
+
{ auth: "BearerJWT", service: "chain" }
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
getDashboardContract() {
|
|
539
|
+
return this.req("GET", "/v5/dashboard/contract", {
|
|
540
|
+
auth: "BearerJWT",
|
|
541
|
+
service: "chain"
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
getDashboardWitnesses(params) {
|
|
545
|
+
return this.req("GET", "/v5/dashboard/witnesses", {
|
|
546
|
+
auth: "BearerJWT",
|
|
547
|
+
service: "chain",
|
|
548
|
+
query: params
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
552
|
+
// OAuth 2.1 + PKCE flow helpers (Phase AUTH Thread A)
|
|
553
|
+
//
|
|
554
|
+
// Designed for browser SPAs without a backend ("Sign in with Witnium" for a
|
|
555
|
+
// Lovable customer). The flow:
|
|
556
|
+
//
|
|
557
|
+
// 1. App calls `beginOAuthLogin({ redirectUri })`, gets a URL, redirects.
|
|
558
|
+
// 2. User authenticates at auth.witniumchain.com, server redirects back
|
|
559
|
+
// to the app's redirectUri with `?code=…&state=…`.
|
|
560
|
+
// 3. App calls `completeOAuthLogin(window.location.href)`, gets back an
|
|
561
|
+
// access token. The SDK stashes it in memory; subsequent `BearerJWT`
|
|
562
|
+
// calls use it transparently.
|
|
563
|
+
// 4. When a `BearerJWT` call returns 401 (token expired), `req()` calls
|
|
564
|
+
// `refreshAccessToken` once and retries. Caller never sees the 401.
|
|
565
|
+
//
|
|
566
|
+
// Access tokens live in memory only — never localStorage. They die on tab
|
|
567
|
+
// close; the refresh token (held in memory today, planned to migrate to an
|
|
568
|
+
// HttpOnly cookie set by /token server-side) survives long enough for a
|
|
569
|
+
// silent refresh on the next /completeOAuthLogin or 401-retry. Refresh
|
|
570
|
+
// tokens rotate on every use, so a single-flight gate avoids racing two
|
|
571
|
+
// concurrent refreshes against the same token.
|
|
572
|
+
//
|
|
573
|
+
// Endpoint discovery: the SDK fetches /.well-known/openid-configuration
|
|
574
|
+
// from `baseUrl` once and reuses the parsed result. oidc-provider's paths
|
|
575
|
+
// (/auth, /token, /jwks, etc.) are not hard-coded — if accounts ever moves
|
|
576
|
+
// them, only the discovery doc has to be right.
|
|
577
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
578
|
+
/**
|
|
579
|
+
* Build the authorization-server URL for the start of an OAuth login flow.
|
|
580
|
+
*
|
|
581
|
+
* Generates a fresh PKCE verifier + challenge, stashes the verifier in the
|
|
582
|
+
* configured {@link PkceVerifierStorage} under the `state` key, and returns
|
|
583
|
+
* the URL the caller should redirect the user to. Side-effects:
|
|
584
|
+
*
|
|
585
|
+
* - sessionStorage gets a PKCE entry under `witniumchain.pkce.<state>`.
|
|
586
|
+
* - The client instance remembers the `redirectUri` for this `state`
|
|
587
|
+
* so {@link completeOAuthLogin} can rebuild the matching token request
|
|
588
|
+
* without the caller threading the URI through twice.
|
|
589
|
+
*
|
|
590
|
+
* The caller is responsible for the redirect itself
|
|
591
|
+
* (`window.location.assign(result.authorizationUrl)`); the SDK doesn't
|
|
592
|
+
* touch `window` directly so SSR + non-browser callers are not broken.
|
|
593
|
+
*/
|
|
594
|
+
async beginOAuthLogin(args) {
|
|
595
|
+
if (!this.oauthClientId) {
|
|
596
|
+
throw new WitniumchainApiError({
|
|
597
|
+
status: 0,
|
|
598
|
+
message: "WitniumchainClient: beginOAuthLogin requires `oauthClientId` to be set on the constructor."
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
const discovery = await this.fetchDiscovery();
|
|
602
|
+
const state = args.state ?? generateState();
|
|
603
|
+
const verifier = generateCodeVerifier();
|
|
604
|
+
const challenge = await deriveCodeChallenge(verifier);
|
|
605
|
+
const scope = (args.scope ?? ["openid", "profile", "email"]).join(" ");
|
|
606
|
+
const url = new URL(discovery.authorization_endpoint);
|
|
607
|
+
url.searchParams.set("response_type", "code");
|
|
608
|
+
url.searchParams.set("client_id", this.oauthClientId);
|
|
609
|
+
url.searchParams.set("redirect_uri", args.redirectUri);
|
|
610
|
+
url.searchParams.set("scope", scope);
|
|
611
|
+
url.searchParams.set("state", state);
|
|
612
|
+
url.searchParams.set("code_challenge", challenge);
|
|
613
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
614
|
+
if (args.prompt) url.searchParams.set("prompt", args.prompt);
|
|
615
|
+
this.verifierStorageOrDefault().set(state, verifier);
|
|
616
|
+
this.pendingLogins.set(state, { redirectUri: args.redirectUri });
|
|
617
|
+
return { authorizationUrl: url.toString(), state };
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Exchange an authorization-code callback for an access token.
|
|
621
|
+
*
|
|
622
|
+
* Reads `code` and `state` from the callback URL, validates the state
|
|
623
|
+
* against a stored verifier, exchanges the code at the token endpoint,
|
|
624
|
+
* and stores the access token (and refresh token) in the client's
|
|
625
|
+
* in-memory state. Returns the access token + its expiry for callers
|
|
626
|
+
* who want to display the session or schedule a proactive refresh.
|
|
627
|
+
*
|
|
628
|
+
* Throws (without consuming the verifier) when the callback URL is
|
|
629
|
+
* missing `code` or `state`, or when the state has no matching verifier
|
|
630
|
+
* — the latter happens when the user opens an old/forged callback URL,
|
|
631
|
+
* or when sessionStorage was cleared between authorize and callback.
|
|
632
|
+
*
|
|
633
|
+
* The caller is responsible for stripping `code` and `state` from the
|
|
634
|
+
* browser URL afterwards (`window.history.replaceState`) so a refresh
|
|
635
|
+
* doesn't re-trigger the exchange against an already-consumed code.
|
|
636
|
+
*/
|
|
637
|
+
async completeOAuthLogin(callbackUrl) {
|
|
638
|
+
if (!this.oauthClientId) {
|
|
639
|
+
throw new WitniumchainApiError({
|
|
640
|
+
status: 0,
|
|
641
|
+
message: "WitniumchainClient: completeOAuthLogin requires `oauthClientId` to be set on the constructor."
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
const url = callbackUrl instanceof URL ? callbackUrl : new URL(callbackUrl);
|
|
645
|
+
const code = url.searchParams.get("code");
|
|
646
|
+
const state = url.searchParams.get("state");
|
|
647
|
+
const error = url.searchParams.get("error");
|
|
648
|
+
if (error) {
|
|
649
|
+
throw new WitniumchainApiError({
|
|
650
|
+
status: 0,
|
|
651
|
+
message: `OAuth authorize returned error: ${error}${url.searchParams.get("error_description") ? ` (${url.searchParams.get("error_description")})` : ""}`,
|
|
652
|
+
errorLabel: error
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
if (!code || !state) {
|
|
656
|
+
throw new WitniumchainApiError({
|
|
657
|
+
status: 0,
|
|
658
|
+
message: "WitniumchainClient: callbackUrl missing required `code` and/or `state` parameters."
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
const storage = this.verifierStorageOrDefault();
|
|
662
|
+
const verifier = storage.get(state);
|
|
663
|
+
if (!verifier) {
|
|
664
|
+
throw new WitniumchainApiError({
|
|
665
|
+
status: 0,
|
|
666
|
+
message: "WitniumchainClient: no PKCE verifier stored for this `state`. The login flow either was not started in this tab, was already completed, or the sessionStorage entry was cleared."
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
const pending = this.pendingLogins.get(state);
|
|
670
|
+
if (!pending) {
|
|
671
|
+
storage.remove(state);
|
|
672
|
+
throw new WitniumchainApiError({
|
|
673
|
+
status: 0,
|
|
674
|
+
message: "WitniumchainClient: PKCE verifier exists but the redirectUri for this `state` was not found in client memory (the client instance was replaced between beginOAuthLogin and completeOAuthLogin)."
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
const discovery = await this.fetchDiscovery();
|
|
678
|
+
const form = new URLSearchParams();
|
|
679
|
+
form.set("grant_type", "authorization_code");
|
|
680
|
+
form.set("code", code);
|
|
681
|
+
form.set("redirect_uri", pending.redirectUri);
|
|
682
|
+
form.set("client_id", this.oauthClientId);
|
|
683
|
+
form.set("code_verifier", verifier);
|
|
684
|
+
const tokens = await this.postTokenEndpoint(discovery.token_endpoint, form);
|
|
685
|
+
storage.remove(state);
|
|
686
|
+
this.pendingLogins.delete(state);
|
|
687
|
+
return this.persistTokenResponse(tokens);
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Refresh the access token. Sends `grant_type=refresh_token` to the token
|
|
691
|
+
* endpoint with the in-memory refresh token, and updates `accessToken`
|
|
692
|
+
* (and the rotated refresh token) on success.
|
|
693
|
+
*
|
|
694
|
+
* Concurrent callers — including the 401-retry interceptor inside `req()`
|
|
695
|
+
* fanning out N parallel calls — share a single in-flight refresh promise:
|
|
696
|
+
* refresh tokens rotate on use, so issuing two concurrent refreshes would
|
|
697
|
+
* race and one would 401 with `invalid_grant`.
|
|
698
|
+
*
|
|
699
|
+
* Throws `WitniumchainApiError` if the refresh token is missing (the SDK
|
|
700
|
+
* was constructed without one and `completeOAuthLogin` was never called)
|
|
701
|
+
* or if the AS rejects the refresh (token revoked / expired). On rejection
|
|
702
|
+
* the in-memory tokens are cleared so subsequent BearerJWT calls fail fast
|
|
703
|
+
* instead of retrying with a now-invalid token.
|
|
704
|
+
*/
|
|
705
|
+
async refreshAccessToken() {
|
|
706
|
+
if (this.refreshInFlight) return this.refreshInFlight;
|
|
707
|
+
this.refreshInFlight = (async () => {
|
|
708
|
+
try {
|
|
709
|
+
return await this.refreshAccessTokenInternal();
|
|
710
|
+
} finally {
|
|
711
|
+
this.refreshInFlight = void 0;
|
|
712
|
+
}
|
|
713
|
+
})();
|
|
714
|
+
return this.refreshInFlight;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* End the OAuth session.
|
|
718
|
+
*
|
|
719
|
+
* Clears the in-memory access + refresh tokens. Best-effort revocation of
|
|
720
|
+
* the server-side session would require a server endpoint that accepts a
|
|
721
|
+
* Bearer-token-authenticated DELETE (today's `/v1/oauth/sessions` requires
|
|
722
|
+
* the first-party `wac_session` cookie, see oauth-sessions.controller.ts).
|
|
723
|
+
* That endpoint will arrive with Phase AUTH Thread E; until then,
|
|
724
|
+
* `signOut` clears local state only and the access token's natural TTL
|
|
725
|
+
* (currently 60 min) bounds residual risk if the refresh token is also
|
|
726
|
+
* dropped — which it is, here.
|
|
727
|
+
*/
|
|
728
|
+
signOut() {
|
|
729
|
+
this.accessToken = void 0;
|
|
730
|
+
this.refreshToken = void 0;
|
|
731
|
+
this.hasRefreshCookie = false;
|
|
732
|
+
this.pendingLogins.clear();
|
|
733
|
+
}
|
|
734
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
735
|
+
// Internal: OAuth flow helpers
|
|
736
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
737
|
+
verifierStorageOrDefault() {
|
|
738
|
+
if (this.verifierStorage) return this.verifierStorage;
|
|
739
|
+
return defaultVerifierStorage();
|
|
740
|
+
}
|
|
741
|
+
async fetchDiscovery() {
|
|
742
|
+
if (this.discoveryCache) return this.discoveryCache;
|
|
743
|
+
this.discoveryCache = (async () => {
|
|
744
|
+
const url = `${this.baseUrl}/.well-known/openid-configuration`;
|
|
745
|
+
let res;
|
|
746
|
+
try {
|
|
747
|
+
res = await this.fetchImpl(url, {
|
|
748
|
+
method: "GET",
|
|
749
|
+
headers: { accept: "application/json" }
|
|
750
|
+
});
|
|
751
|
+
} catch (err) {
|
|
752
|
+
this.discoveryCache = void 0;
|
|
753
|
+
throw new WitniumchainApiError({
|
|
754
|
+
status: 0,
|
|
755
|
+
message: err instanceof Error ? `OIDC discovery fetch failed: ${err.message}` : "OIDC discovery fetch failed"
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
if (!res.ok) {
|
|
759
|
+
this.discoveryCache = void 0;
|
|
760
|
+
throw new WitniumchainApiError({
|
|
761
|
+
status: res.status,
|
|
762
|
+
message: `OIDC discovery fetch failed: HTTP ${res.status}`
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
const parsed = await res.json();
|
|
766
|
+
if (!parsed.authorization_endpoint || !parsed.token_endpoint) {
|
|
767
|
+
this.discoveryCache = void 0;
|
|
768
|
+
throw new WitniumchainApiError({
|
|
769
|
+
status: 0,
|
|
770
|
+
message: "OIDC discovery doc is missing `authorization_endpoint` or `token_endpoint`."
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
return {
|
|
774
|
+
authorization_endpoint: parsed.authorization_endpoint,
|
|
775
|
+
token_endpoint: parsed.token_endpoint,
|
|
776
|
+
issuer: parsed.issuer ?? this.baseUrl
|
|
777
|
+
};
|
|
778
|
+
})();
|
|
779
|
+
return this.discoveryCache;
|
|
780
|
+
}
|
|
781
|
+
async postTokenEndpoint(endpoint, form) {
|
|
782
|
+
const controller = new AbortController();
|
|
783
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
784
|
+
let res;
|
|
785
|
+
try {
|
|
786
|
+
res = await this.fetchImpl(endpoint, {
|
|
787
|
+
method: "POST",
|
|
788
|
+
headers: {
|
|
789
|
+
accept: "application/json",
|
|
790
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
791
|
+
},
|
|
792
|
+
body: form.toString(),
|
|
793
|
+
signal: controller.signal,
|
|
794
|
+
// Sends the HttpOnly refresh-token cookie when the server starts
|
|
795
|
+
// setting it (planned server-side change); a no-op until then.
|
|
796
|
+
credentials: "include"
|
|
797
|
+
});
|
|
798
|
+
} catch (err) {
|
|
799
|
+
throw new WitniumchainApiError({
|
|
800
|
+
status: 0,
|
|
801
|
+
message: err instanceof Error ? `Token endpoint fetch failed: ${err.message}` : "Token endpoint fetch failed"
|
|
802
|
+
});
|
|
803
|
+
} finally {
|
|
804
|
+
clearTimeout(timer);
|
|
805
|
+
}
|
|
806
|
+
const text = await res.text();
|
|
807
|
+
let parsed = null;
|
|
808
|
+
if (text.length > 0) {
|
|
809
|
+
try {
|
|
810
|
+
parsed = JSON.parse(text);
|
|
811
|
+
} catch {
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (!res.ok) {
|
|
815
|
+
throw this.parseApiError(res.status, parsed, text);
|
|
816
|
+
}
|
|
817
|
+
return parsed;
|
|
818
|
+
}
|
|
819
|
+
persistTokenResponse(tokens) {
|
|
820
|
+
if (!tokens.access_token) {
|
|
821
|
+
throw new WitniumchainApiError({
|
|
822
|
+
status: 0,
|
|
823
|
+
message: "Token endpoint response missing `access_token`."
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
this.accessToken = tokens.access_token;
|
|
827
|
+
if (tokens.refresh_token) {
|
|
828
|
+
this.refreshToken = tokens.refresh_token;
|
|
829
|
+
} else {
|
|
830
|
+
this.refreshToken = void 0;
|
|
831
|
+
this.hasRefreshCookie = true;
|
|
832
|
+
}
|
|
833
|
+
const ttlSeconds = typeof tokens.expires_in === "number" ? tokens.expires_in : 3600;
|
|
834
|
+
const expiresAt = Math.floor(Date.now() / 1e3) + ttlSeconds;
|
|
835
|
+
return { accessToken: tokens.access_token, expiresAt };
|
|
836
|
+
}
|
|
837
|
+
async refreshAccessTokenInternal() {
|
|
838
|
+
if (!this.oauthClientId) {
|
|
839
|
+
throw new WitniumchainApiError({
|
|
840
|
+
status: 0,
|
|
841
|
+
message: "WitniumchainClient: refreshAccessToken requires `oauthClientId` to be set on the constructor."
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
if (!this.refreshToken && !this.hasRefreshCookie) {
|
|
845
|
+
throw new WitniumchainApiError({
|
|
846
|
+
status: 0,
|
|
847
|
+
message: "WitniumchainClient: no refresh credential available. Call `completeOAuthLogin` first \u2014 the server delivers the refresh token either in the response body (Node) or as an HttpOnly cookie (browser)."
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
const discovery = await this.fetchDiscovery();
|
|
851
|
+
const form = new URLSearchParams();
|
|
852
|
+
form.set("grant_type", "refresh_token");
|
|
853
|
+
form.set("client_id", this.oauthClientId);
|
|
854
|
+
if (this.refreshToken) {
|
|
855
|
+
form.set("refresh_token", this.refreshToken);
|
|
856
|
+
}
|
|
857
|
+
try {
|
|
858
|
+
const tokens = await this.postTokenEndpoint(discovery.token_endpoint, form);
|
|
859
|
+
return this.persistTokenResponse(tokens);
|
|
860
|
+
} catch (err) {
|
|
861
|
+
this.accessToken = void 0;
|
|
862
|
+
this.refreshToken = void 0;
|
|
863
|
+
this.hasRefreshCookie = false;
|
|
864
|
+
throw err;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
328
867
|
// ────────────────────────────────────────────────────────────────────────
|
|
329
868
|
// Internal: fetch wrapper that maps non-2xx → WitniumchainApiError
|
|
330
869
|
// and applies the configured credential to the request.
|
|
331
870
|
// ────────────────────────────────────────────────────────────────────────
|
|
332
871
|
async req(method, path, opts) {
|
|
333
|
-
const
|
|
872
|
+
const pathWithQuery = this.buildPathWithQuery(path, opts.query);
|
|
873
|
+
const base = this.resolveBaseUrl(opts.service);
|
|
874
|
+
const url = `${base}${pathWithQuery}`;
|
|
334
875
|
const headers = {
|
|
335
876
|
accept: "application/json",
|
|
336
877
|
...opts.headers ?? {}
|
|
@@ -339,7 +880,13 @@ var WitniumchainClient = class {
|
|
|
339
880
|
headers["content-type"] = "application/json";
|
|
340
881
|
}
|
|
341
882
|
const bodyString = opts.body !== void 0 ? JSON.stringify(opts.body) : void 0;
|
|
342
|
-
await this.applyAuth(
|
|
883
|
+
await this.applyAuth(
|
|
884
|
+
headers,
|
|
885
|
+
opts.auth,
|
|
886
|
+
method,
|
|
887
|
+
pathWithQuery,
|
|
888
|
+
bodyString ?? ""
|
|
889
|
+
);
|
|
343
890
|
const controller = new AbortController();
|
|
344
891
|
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
345
892
|
let res;
|
|
@@ -356,13 +903,16 @@ var WitniumchainClient = class {
|
|
|
356
903
|
} catch (err) {
|
|
357
904
|
throw new WitniumchainApiError({
|
|
358
905
|
status: 0,
|
|
359
|
-
message: err instanceof Error ? `Network error contacting ${
|
|
906
|
+
message: err instanceof Error ? `Network error contacting ${base}: ${err.message}` : `Network error contacting ${base}`
|
|
360
907
|
});
|
|
361
908
|
} finally {
|
|
362
909
|
clearTimeout(timer);
|
|
363
910
|
}
|
|
364
911
|
if (opts.expectNoContent) {
|
|
365
912
|
if (!res.ok) {
|
|
913
|
+
if (await this.shouldRefreshAndRetry(res, opts)) {
|
|
914
|
+
return this.req(method, path, { ...opts, _isRetry: true });
|
|
915
|
+
}
|
|
366
916
|
throw await this.toApiError(res);
|
|
367
917
|
}
|
|
368
918
|
return void 0;
|
|
@@ -376,18 +926,93 @@ var WitniumchainClient = class {
|
|
|
376
926
|
}
|
|
377
927
|
}
|
|
378
928
|
if (!res.ok) {
|
|
929
|
+
if (await this.shouldRefreshAndRetryParsed(res.status, parsed, opts)) {
|
|
930
|
+
return this.req(method, path, { ...opts, _isRetry: true });
|
|
931
|
+
}
|
|
379
932
|
throw this.parseApiError(res.status, parsed, text);
|
|
380
933
|
}
|
|
381
934
|
return parsed;
|
|
382
935
|
}
|
|
383
|
-
|
|
384
|
-
|
|
936
|
+
/**
|
|
937
|
+
* Decide whether a non-2xx response on a `BearerJWT` route should trigger
|
|
938
|
+
* a refresh + single retry. Returns false (no retry) when:
|
|
939
|
+
*
|
|
940
|
+
* - this call IS the retry — never recurse,
|
|
941
|
+
* - the auth mode isn't BearerJWT — refresh only helps Bearer tokens,
|
|
942
|
+
* - the status isn't 401 — refresh doesn't unstick 403/404/500/etc,
|
|
943
|
+
* - we don't have a refresh token in memory — nothing to refresh with,
|
|
944
|
+
* - the response body doesn't look like an expired-token signal.
|
|
945
|
+
*
|
|
946
|
+
* On a positive answer, the method ALSO performs the refresh in-line:
|
|
947
|
+
* the caller just gets back `true` and replays the original request,
|
|
948
|
+
* which picks up the freshly-rotated `accessToken` via `applyAuth`.
|
|
949
|
+
* Refresh failures are swallowed here (the caller falls through to the
|
|
950
|
+
* regular error path); `refreshAccessTokenInternal` already cleared the
|
|
951
|
+
* in-memory tokens so the retry won't have anything to send.
|
|
952
|
+
*/
|
|
953
|
+
async shouldRefreshAndRetry(res, opts) {
|
|
954
|
+
if (opts._isRetry) return false;
|
|
955
|
+
if (opts.auth !== "BearerJWT") return false;
|
|
956
|
+
if (res.status !== 401) return false;
|
|
957
|
+
if (!this.refreshToken && !this.hasRefreshCookie) return false;
|
|
958
|
+
try {
|
|
959
|
+
await this.refreshAccessToken();
|
|
960
|
+
return true;
|
|
961
|
+
} catch {
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Same decision as `shouldRefreshAndRetry` but for the JSON-parsed path,
|
|
967
|
+
* where we have the body. Adds one extra gate: only retry when the parsed
|
|
968
|
+
* body looks like a "token expired" signal (`error: 'token_expired'` per
|
|
969
|
+
* the AUTH plan, or `error: 'invalid_token'` per RFC 6750 §3.1). A 401
|
|
970
|
+
* with any other `error` label is a real authn/authz failure that refresh
|
|
971
|
+
* won't fix — surface it instead of burning a refresh token.
|
|
972
|
+
*/
|
|
973
|
+
async shouldRefreshAndRetryParsed(status, parsed, opts) {
|
|
974
|
+
if (opts._isRetry) return false;
|
|
975
|
+
if (opts.auth !== "BearerJWT") return false;
|
|
976
|
+
if (status !== 401) return false;
|
|
977
|
+
if (!this.refreshToken && !this.hasRefreshCookie) return false;
|
|
978
|
+
const body = parsed;
|
|
979
|
+
const label = typeof body?.error === "string" ? body.error : void 0;
|
|
980
|
+
if (label !== void 0 && label !== "token_expired" && label !== "invalid_token") {
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
983
|
+
try {
|
|
984
|
+
await this.refreshAccessToken();
|
|
985
|
+
return true;
|
|
986
|
+
} catch {
|
|
987
|
+
return false;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Resolve which base URL to call. Default is accounts (`baseUrl`). When a
|
|
992
|
+
* method opts into 'chain', `chainBaseUrl` must be configured — throw at call
|
|
993
|
+
* time with a clear message rather than fall back silently to accounts (no
|
|
994
|
+
* defaults: a wrong base URL is a real bug and would mask itself as a 404).
|
|
995
|
+
*/
|
|
996
|
+
resolveBaseUrl(service) {
|
|
997
|
+
if (service === "chain") {
|
|
998
|
+
if (!this.chainBaseUrl) {
|
|
999
|
+
throw new WitniumchainApiError({
|
|
1000
|
+
status: 0,
|
|
1001
|
+
message: 'WitniumchainClient: chain-api method called without `chainBaseUrl` configured. Pass `chainBaseUrl: "https://api.witniumchain.com"` (or your environment\'s chain-api URL) to the client constructor.'
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
return this.chainBaseUrl;
|
|
1005
|
+
}
|
|
1006
|
+
return this.baseUrl;
|
|
1007
|
+
}
|
|
1008
|
+
buildPathWithQuery(path, query) {
|
|
1009
|
+
if (!query) return path;
|
|
385
1010
|
const qs = new URLSearchParams();
|
|
386
1011
|
for (const [k, v] of Object.entries(query)) {
|
|
387
1012
|
if (v !== void 0) qs.set(k, String(v));
|
|
388
1013
|
}
|
|
389
1014
|
const suffix = qs.toString();
|
|
390
|
-
return suffix ? `${
|
|
1015
|
+
return suffix ? `${path}?${suffix}` : path;
|
|
391
1016
|
}
|
|
392
1017
|
async applyAuth(headers, auth, method, path, bodyString) {
|
|
393
1018
|
switch (auth) {
|
|
@@ -400,12 +1025,12 @@ var WitniumchainClient = class {
|
|
|
400
1025
|
return;
|
|
401
1026
|
}
|
|
402
1027
|
case "BearerJWT": {
|
|
403
|
-
if (!this.
|
|
1028
|
+
if (!this.accessToken) {
|
|
404
1029
|
throw new Error(
|
|
405
|
-
`WitniumchainClient: ${method} ${path} requires an OAuth access token. Pass \`accessToken\` to the constructor.`
|
|
1030
|
+
`WitniumchainClient: ${method} ${path} requires an OAuth access token. Pass \`accessToken\` to the constructor, or call \`beginOAuthLogin\`/\`completeOAuthLogin\` to obtain one.`
|
|
406
1031
|
);
|
|
407
1032
|
}
|
|
408
|
-
headers["authorization"] = `Bearer ${this.
|
|
1033
|
+
headers["authorization"] = `Bearer ${this.accessToken}`;
|
|
409
1034
|
return;
|
|
410
1035
|
}
|
|
411
1036
|
case "OrgApiKey": {
|
|
@@ -597,17 +1222,71 @@ var OauthSessions = class {
|
|
|
597
1222
|
return this.client.revokeAllOauthSessions();
|
|
598
1223
|
}
|
|
599
1224
|
};
|
|
1225
|
+
var MfaNamespace = class {
|
|
1226
|
+
totp;
|
|
1227
|
+
recoveryCodes;
|
|
1228
|
+
constructor(client) {
|
|
1229
|
+
this.totp = new MfaTotp(client);
|
|
1230
|
+
this.recoveryCodes = new MfaRecoveryCodes(client);
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
var MfaTotp = class {
|
|
1234
|
+
constructor(client) {
|
|
1235
|
+
this.client = client;
|
|
1236
|
+
}
|
|
1237
|
+
client;
|
|
1238
|
+
/**
|
|
1239
|
+
* Start enrolment. Returns the secret + otpauth URL — render the URL as a
|
|
1240
|
+
* QR code in your dashboard (any QR library will do; the SDK doesn't bundle
|
|
1241
|
+
* one). The enrolment is NOT yet a usable second factor: call
|
|
1242
|
+
* {@link confirm} with the first 6-digit code from the authenticator app
|
|
1243
|
+
* to activate it AND receive the recovery codes.
|
|
1244
|
+
*/
|
|
1245
|
+
enroll() {
|
|
1246
|
+
return this.client.enrollTotp();
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Confirm enrolment with the first user-supplied code. Returns the 10
|
|
1250
|
+
* single-use recovery codes — show them to the user ONCE; the server never
|
|
1251
|
+
* returns them again. Throws `WitniumchainApiError` with status 400 when
|
|
1252
|
+
* the code is invalid or the enrolment is already confirmed.
|
|
1253
|
+
*/
|
|
1254
|
+
confirm(code) {
|
|
1255
|
+
return this.client.confirmTotp({ code });
|
|
1256
|
+
}
|
|
1257
|
+
/** Disable TOTP, wiping both the secret and all recovery codes. */
|
|
1258
|
+
disable() {
|
|
1259
|
+
return this.client.disableTotp();
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
var MfaRecoveryCodes = class {
|
|
1263
|
+
constructor(client) {
|
|
1264
|
+
this.client = client;
|
|
1265
|
+
}
|
|
1266
|
+
client;
|
|
1267
|
+
/**
|
|
1268
|
+
* Issue a fresh set of 10 recovery codes, invalidating the prior ones.
|
|
1269
|
+
* Same as `confirm` — codes are returned ONCE and never readable again.
|
|
1270
|
+
*/
|
|
1271
|
+
regenerate() {
|
|
1272
|
+
return this.client.regenerateRecoveryCodes();
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
600
1275
|
function sleep(ms) {
|
|
601
1276
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
602
1277
|
}
|
|
603
|
-
function
|
|
604
|
-
|
|
605
|
-
if (
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
return
|
|
1278
|
+
function stableStringify(value) {
|
|
1279
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
1280
|
+
if (Array.isArray(value)) {
|
|
1281
|
+
return "[" + value.map(stableStringify).join(",") + "]";
|
|
1282
|
+
}
|
|
1283
|
+
const obj = value;
|
|
1284
|
+
const keys = Object.keys(obj).sort();
|
|
1285
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
|
|
1286
|
+
}
|
|
1287
|
+
async function deriveBodyKey(namespace, contractAddress, body) {
|
|
1288
|
+
const canonical = `${namespace}|${contractAddress.toLowerCase()}|${stableStringify(body)}`;
|
|
1289
|
+
return await sha256Hex(canonical);
|
|
611
1290
|
}
|
|
612
1291
|
async function sha256Hex(input) {
|
|
613
1292
|
const subtle = globalThis.crypto?.subtle;
|
|
@@ -678,6 +1357,106 @@ var WitniumchainAdminClient = class {
|
|
|
678
1357
|
}
|
|
679
1358
|
};
|
|
680
1359
|
|
|
1360
|
+
// src/chain-admin-client.ts
|
|
1361
|
+
var WitniumchainChainAdminClient = class {
|
|
1362
|
+
baseUrl;
|
|
1363
|
+
adminToken;
|
|
1364
|
+
adminTokenProvider;
|
|
1365
|
+
timeout;
|
|
1366
|
+
fetchImpl;
|
|
1367
|
+
constructor(config) {
|
|
1368
|
+
if (!config.baseUrl) {
|
|
1369
|
+
throw new Error("WitniumchainChainAdminClient: baseUrl is required");
|
|
1370
|
+
}
|
|
1371
|
+
if (config.adminToken && config.adminTokenProvider) {
|
|
1372
|
+
throw new Error(
|
|
1373
|
+
"WitniumchainChainAdminClient: pass either adminToken (static) or adminTokenProvider (OAuth), not both"
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
if (!config.adminToken && !config.adminTokenProvider) {
|
|
1377
|
+
throw new Error(
|
|
1378
|
+
"WitniumchainChainAdminClient: one of adminToken or adminTokenProvider is required"
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
1382
|
+
this.adminToken = config.adminToken;
|
|
1383
|
+
this.adminTokenProvider = config.adminTokenProvider;
|
|
1384
|
+
this.timeout = config.timeout ?? 3e4;
|
|
1385
|
+
this.fetchImpl = config.fetch ?? globalThis.fetch;
|
|
1386
|
+
}
|
|
1387
|
+
async deployContract(body) {
|
|
1388
|
+
return this.req("POST", "/v5/contracts/deploy", body);
|
|
1389
|
+
}
|
|
1390
|
+
async addSigningKey(contractAddress, body) {
|
|
1391
|
+
return this.req(
|
|
1392
|
+
"POST",
|
|
1393
|
+
`/v5/contracts/${encodeURIComponent(contractAddress)}/keys`,
|
|
1394
|
+
body
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
async revokeSigningKey(contractAddress, body) {
|
|
1398
|
+
return this.req(
|
|
1399
|
+
"POST",
|
|
1400
|
+
`/v5/contracts/${encodeURIComponent(contractAddress)}/keys/revoke`,
|
|
1401
|
+
body
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
async pauseContract(contractAddress, body) {
|
|
1405
|
+
return this.req(
|
|
1406
|
+
"POST",
|
|
1407
|
+
`/v5/contracts/${encodeURIComponent(contractAddress)}/pause`,
|
|
1408
|
+
body
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
async unpauseContract(contractAddress, body) {
|
|
1412
|
+
return this.req(
|
|
1413
|
+
"POST",
|
|
1414
|
+
`/v5/contracts/${encodeURIComponent(contractAddress)}/unpause`,
|
|
1415
|
+
body
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
async resolveToken() {
|
|
1419
|
+
if (this.adminTokenProvider) return this.adminTokenProvider();
|
|
1420
|
+
return this.adminToken;
|
|
1421
|
+
}
|
|
1422
|
+
async req(method, path, body) {
|
|
1423
|
+
const token = await this.resolveToken();
|
|
1424
|
+
const controller = new AbortController();
|
|
1425
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
1426
|
+
try {
|
|
1427
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
1428
|
+
method,
|
|
1429
|
+
headers: {
|
|
1430
|
+
accept: "application/json",
|
|
1431
|
+
"content-type": "application/json",
|
|
1432
|
+
authorization: `Bearer ${token}`
|
|
1433
|
+
},
|
|
1434
|
+
body: JSON.stringify(body),
|
|
1435
|
+
signal: controller.signal
|
|
1436
|
+
});
|
|
1437
|
+
const text = await response.text();
|
|
1438
|
+
const parsed = text ? safeParse(text) : void 0;
|
|
1439
|
+
if (!response.ok) {
|
|
1440
|
+
throw new WitniumchainApiError({
|
|
1441
|
+
status: response.status,
|
|
1442
|
+
message: parsed?.message ?? `${method} ${path} failed: ${response.status}`,
|
|
1443
|
+
body: parsed
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
return parsed;
|
|
1447
|
+
} finally {
|
|
1448
|
+
clearTimeout(timer);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
function safeParse(text) {
|
|
1453
|
+
try {
|
|
1454
|
+
return JSON.parse(text);
|
|
1455
|
+
} catch {
|
|
1456
|
+
return text;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
681
1460
|
// src/org-client.ts
|
|
682
1461
|
var WitniumchainOrgClient = class {
|
|
683
1462
|
inner;
|
|
@@ -724,6 +1503,6 @@ var OrgUsers = class {
|
|
|
724
1503
|
}
|
|
725
1504
|
};
|
|
726
1505
|
|
|
727
|
-
export { DelegatedKeys, OauthNamespace, OauthSessions, OrgUsers, SigningKeys, Subscriptions, WitniumchainAdminClient, WitniumchainApiError, WitniumchainClient, WitniumchainOrgClient };
|
|
1506
|
+
export { DelegatedKeys, MfaNamespace, MfaRecoveryCodes, MfaTotp, OauthNamespace, OauthSessions, OrgUsers, SigningKeys, Subscriptions, WitniumchainAdminClient, WitniumchainApiError, WitniumchainChainAdminClient, WitniumchainClient, WitniumchainOrgClient, defaultVerifierStorage };
|
|
728
1507
|
//# sourceMappingURL=index.mjs.map
|
|
729
1508
|
//# sourceMappingURL=index.mjs.map
|