@x1id/resolve 0.12.0 → 0.13.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 +38 -0
- package/dist/gateClient.d.ts +11 -1
- package/dist/gateClient.js +11 -1
- package/dist/index.js +194 -7
- package/dist/signin.d.ts +15 -3
- package/dist/signin.js +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -116,6 +116,44 @@ to the other". `@jack.x1` throws `ambiguous` rather than guessing. Use
|
|
|
116
116
|
`looksLikeName(input)` to decide whether to attempt resolution at all, so
|
|
117
117
|
pasting base58 does not surface a validation error.
|
|
118
118
|
|
|
119
|
+
## Scoped names under customer TLDs
|
|
120
|
+
|
|
121
|
+
Anyone can launch their own X1ID TLD (e.g. `.e2eshib`), and names registered
|
|
122
|
+
under it — `hello.e2eshib` — are first-class X1ID names. These are **not** X1NS
|
|
123
|
+
or SNS: `.x1` / `.xnt` / `.xen` are the separate **X1NS** domain system and
|
|
124
|
+
`.sol` is **SNS** (Solana Name Service), each resolved on its own path. A
|
|
125
|
+
customer TLD is resolved natively by this SDK against the `@handle` registry.
|
|
126
|
+
|
|
127
|
+
Scoped resolution derives a per-TLD PDA, which needs `@solana/web3.js`. It is
|
|
128
|
+
**opt-in and explicit** — you wire a deriver in; the SDK never auto-imports
|
|
129
|
+
web3 for you:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { createResolver, WasmResolver, makeScopedHandleDeriver } from "@x1id/resolve";
|
|
133
|
+
import * as web3 from "@solana/web3.js";
|
|
134
|
+
|
|
135
|
+
const wasm = await WasmResolver.fromBytes(/* module bytes */);
|
|
136
|
+
|
|
137
|
+
// `makeScopedHandleDeriver` is ASYNC — await it. Pass the RESULT (not the
|
|
138
|
+
// Promise) as `scopedHandleDeriver`.
|
|
139
|
+
const scopedHandleDeriver = await makeScopedHandleDeriver(web3);
|
|
140
|
+
|
|
141
|
+
const x1id = createResolver({
|
|
142
|
+
rpcUrl: "https://rpc.testnet.x1.xyz",
|
|
143
|
+
wasm,
|
|
144
|
+
scopedHandleDeriver,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await x1id.resolve("hello.e2eshib"); // resolves the scoped name under .e2eshib
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Without `scopedHandleDeriver`, resolving a launched customer TLD throws
|
|
151
|
+
`ResolveError { code: "not-configured" }` even with `@solana/web3.js` installed —
|
|
152
|
+
the deriver is what the SDK actually calls. And because `makeScopedHandleDeriver`
|
|
153
|
+
returns a Promise, passing it **un-awaited** is a common footgun; the SDK
|
|
154
|
+
detects that and throws a clear `not-configured` error telling you to `await` it,
|
|
155
|
+
rather than a cryptic "scopedHandleKey is not a function".
|
|
156
|
+
|
|
119
157
|
## Reads chain state, not an API
|
|
120
158
|
|
|
121
159
|
X1NS names resolve by deriving accounts and reading them over RPC. This library
|
package/dist/gateClient.d.ts
CHANGED
|
@@ -77,7 +77,17 @@ export interface GuardResult {
|
|
|
77
77
|
*
|
|
78
78
|
* Fail-closed by default: if the decision cannot be obtained (network/5xx), the
|
|
79
79
|
* guard RE-THROWS {@link GateError} so your route returns an error rather than
|
|
80
|
-
* silently admitting an unverified caller.
|
|
80
|
+
* silently admitting an unverified caller. `onError: "deny"` instead resolves to
|
|
81
|
+
* a clean `allowed: false` (fail-closed without a throw).
|
|
82
|
+
*
|
|
83
|
+
* ⚠️ SECURITY — `onError: "allow"` FAILS OPEN. On ANY error (network blip,
|
|
84
|
+
* timeout, 5xx, auth failure, a malformed response) it fabricates a
|
|
85
|
+
* `passed: true` decision and admits the caller UNVERIFIED. NEVER use it for a
|
|
86
|
+
* real access gate — anyone who can make your gate call fail (trivial: induce a
|
|
87
|
+
* timeout) then bypasses it completely. It exists ONLY for soft, non-security
|
|
88
|
+
* personalization where a missing verification must not degrade UX (e.g.
|
|
89
|
+
* optionally showing a "verified" flourish). If the gate protects anything —
|
|
90
|
+
* a route, a mint, an airdrop, a write — use the default `"throw"` or `"deny"`.
|
|
81
91
|
*
|
|
82
92
|
* @example
|
|
83
93
|
* ```ts
|
package/dist/gateClient.js
CHANGED
|
@@ -78,7 +78,17 @@ export async function checkGate(cfg, subject, policy) {
|
|
|
78
78
|
*
|
|
79
79
|
* Fail-closed by default: if the decision cannot be obtained (network/5xx), the
|
|
80
80
|
* guard RE-THROWS {@link GateError} so your route returns an error rather than
|
|
81
|
-
* silently admitting an unverified caller.
|
|
81
|
+
* silently admitting an unverified caller. `onError: "deny"` instead resolves to
|
|
82
|
+
* a clean `allowed: false` (fail-closed without a throw).
|
|
83
|
+
*
|
|
84
|
+
* ⚠️ SECURITY — `onError: "allow"` FAILS OPEN. On ANY error (network blip,
|
|
85
|
+
* timeout, 5xx, auth failure, a malformed response) it fabricates a
|
|
86
|
+
* `passed: true` decision and admits the caller UNVERIFIED. NEVER use it for a
|
|
87
|
+
* real access gate — anyone who can make your gate call fail (trivial: induce a
|
|
88
|
+
* timeout) then bypasses it completely. It exists ONLY for soft, non-security
|
|
89
|
+
* personalization where a missing verification must not degrade UX (e.g.
|
|
90
|
+
* optionally showing a "verified" flourish). If the gate protects anything —
|
|
91
|
+
* a route, a mint, an airdrop, a write — use the default `"throw"` or `"deny"`.
|
|
82
92
|
*
|
|
83
93
|
* @example
|
|
84
94
|
* ```ts
|
package/dist/index.js
CHANGED
|
@@ -70,6 +70,7 @@ import { scopedTldCandidate, namespaceIsActive, NAMESPACE_MAX_LEN, } from "./sco
|
|
|
70
70
|
import { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
71
71
|
import { DEFAULT_HANDLE_PROGRAM, TOKEN_ACCOUNT_MIN_LEN, bytesEqual, makeAccountReader, nftHolder, parseHandleAccount, readI64, readU64, } from "./accounts.js";
|
|
72
72
|
import { fetchRecords, liveRecords } from "./records.js";
|
|
73
|
+
import { fetchSubnames, resolveSubnameRecords } from "./subname.js";
|
|
73
74
|
/** Root authority for each X1NS TLD — used to verify a fetched account really
|
|
74
75
|
* belongs to the TLD it claims. Without this check a caller handed an
|
|
75
76
|
* arbitrary account would read an owner straight out of it. */
|
|
@@ -79,6 +80,47 @@ const TLD_ROOT = Object.freeze({
|
|
|
79
80
|
xen: "3SUwpSz33AsyJwf6B48cKZuDTswuUEdUhcXszZrFWPqo",
|
|
80
81
|
});
|
|
81
82
|
const SPL_NAME_HEADER_LEN = 96;
|
|
83
|
+
/**
|
|
84
|
+
* Suffixes that are NEVER a subname parent: the native X1NS namespaces (handled
|
|
85
|
+
* by `parseName` before we ever reach the subname path) and the external TLDs
|
|
86
|
+
* the universal resolver owns. `foo.sol`/`foo.eth` must stay `unrecognized` to
|
|
87
|
+
* the native resolver — never resolved as a subname under `@sol`/`@eth` — so
|
|
88
|
+
* the whole never-conflate-a-namespace rule holds on the money path too.
|
|
89
|
+
*/
|
|
90
|
+
const SUBNAME_RESERVED_SUFFIXES = new Set([
|
|
91
|
+
"x1",
|
|
92
|
+
"xnt",
|
|
93
|
+
"xen",
|
|
94
|
+
"sol",
|
|
95
|
+
"eth",
|
|
96
|
+
]);
|
|
97
|
+
/**
|
|
98
|
+
* Classify `input` as a `label.parent` SUBNAME candidate by SHAPE ONLY (no
|
|
99
|
+
* chain read) — the FIRST interpretation tried for a dotted, non-`@`, non-X1NS
|
|
100
|
+
* name (owner-approved disambiguation order: subname before scoped-TLD). The
|
|
101
|
+
* shape is a single interior dot with non-empty halves, a `parent` that is not
|
|
102
|
+
* a reserved suffix, and a `label` with no further dot (a deeper `a.b.c` is
|
|
103
|
+
* sub-subname territory the one-level program does not support → stays
|
|
104
|
+
* `unrecognized`). Whether `@parent` is actually a registered handle is a chain
|
|
105
|
+
* fact the resolver establishes separately. Both halves are normalized for real
|
|
106
|
+
* at resolve; a parent that cannot be a handle (e.g. > 32 bytes) simply fails
|
|
107
|
+
* the registration check and falls through to the scoped path.
|
|
108
|
+
*/
|
|
109
|
+
function subnameCandidate(input) {
|
|
110
|
+
const t = input.trim().toLowerCase();
|
|
111
|
+
if (!t || t.startsWith("@"))
|
|
112
|
+
return null;
|
|
113
|
+
const dot = t.lastIndexOf(".");
|
|
114
|
+
if (dot <= 0 || dot === t.length - 1)
|
|
115
|
+
return null;
|
|
116
|
+
const label = t.slice(0, dot);
|
|
117
|
+
const parent = t.slice(dot + 1);
|
|
118
|
+
if (label.includes("."))
|
|
119
|
+
return null; // a.b.c — a true sub-subname, unsupported
|
|
120
|
+
if (SUBNAME_RESERVED_SUFFIXES.has(parent))
|
|
121
|
+
return null; // never conflate with X1NS / external
|
|
122
|
+
return { label, parent };
|
|
123
|
+
}
|
|
82
124
|
// `Primary` pointer account (`["primary", owner]` under the registry):
|
|
83
125
|
// disc(8) | owner(32) | handle(32) | set_at(i64 LE, 8) | bump(1) = 81 bytes
|
|
84
126
|
const PRIMARY_LEN = 81;
|
|
@@ -240,6 +282,15 @@ export function createResolver(config) {
|
|
|
240
282
|
if (!scopedDeriver) {
|
|
241
283
|
throw new ResolveError("not-configured", `.${label} is a launched X1ID TLD, but scoped resolution needs a PDA deriver — install @solana/web3.js and pass scopedHandleDeriver (see makeScopedHandleDeriver)`, input);
|
|
242
284
|
}
|
|
285
|
+
// `makeScopedHandleDeriver(web3)` is async; a common footgun is passing the
|
|
286
|
+
// un-awaited Promise as `scopedHandleDeriver`, which would otherwise surface
|
|
287
|
+
// a baffling `scopedDeriver.scopedHandleKey is not a function` wrapped as an
|
|
288
|
+
// rpc-error. Detect the Promise/thenable (or any object missing the method)
|
|
289
|
+
// and fail with an actionable message instead.
|
|
290
|
+
if (typeof scopedDeriver.then === "function" ||
|
|
291
|
+
typeof scopedDeriver.scopedHandleKey !== "function") {
|
|
292
|
+
throw new ResolveError("not-configured", "scopedHandleDeriver must be the awaited result of makeScopedHandleDeriver(web3), not the Promise it returns — did you forget `await`?", input);
|
|
293
|
+
}
|
|
243
294
|
let pda;
|
|
244
295
|
try {
|
|
245
296
|
pda = await scopedDeriver.scopedHandleKey(label, name, programBase58);
|
|
@@ -290,6 +341,125 @@ export function createResolver(config) {
|
|
|
290
341
|
cache.set(`scoped:${label}:${name}:${chain}`, { value, expires: Date.now() + ttl });
|
|
291
342
|
return value;
|
|
292
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* Resolve a `label.parent` SUBNAME (#7375) — a label under a parent
|
|
346
|
+
* `@handle`, e.g. `pay.alice`. Called ONLY for a dotted, non-`@`, non-X1NS
|
|
347
|
+
* input (the native parser's `unrecognized` fall-through), and BEFORE the
|
|
348
|
+
* scoped customer-TLD path, per the owner-approved disambiguation order.
|
|
349
|
+
*
|
|
350
|
+
* Returns `null` — the signal to try the scoped path next — when `@parent`
|
|
351
|
+
* is NOT a registered handle (so `x.y` was never a subname). Once `@parent`
|
|
352
|
+
* IS a registered handle we are COMMITTED to the subname interpretation:
|
|
353
|
+
* every outcome is a `Resolved` or a throw (`not-found` / `no-record-for-chain`),
|
|
354
|
+
* never a fall-through to scoped.
|
|
355
|
+
*
|
|
356
|
+
* A subname has NO owner; it resolves PURELY through its per-chain `Record`s
|
|
357
|
+
* (like an ETH/BTC record on a handle), so EVERY chain — X1/SOL included —
|
|
358
|
+
* comes from a record, never an owner key. Both staleness layers of
|
|
359
|
+
* docs/record-trust.md are enforced via `subname.ts`:
|
|
360
|
+
* 1. `subname.createdAt >= parent.registeredAt` — else the subname was left
|
|
361
|
+
* behind by a PREVIOUS owner of the parent (any transfer/sale/recovery/
|
|
362
|
+
* re-registration bumps `registered_at`): `not-found`, never resolved.
|
|
363
|
+
* 2. `record.updatedAt >= subname.createdAt` — a record from a previous
|
|
364
|
+
* incarnation of the same `["subname", parent, label]` PDA is dropped.
|
|
365
|
+
* The subname account itself is located by the program-scoped
|
|
366
|
+
* `getProgramAccounts` scan (`fetchSubnames`) — scoping the scan to the
|
|
367
|
+
* registry program id IS the ownership check — then matched by canonical
|
|
368
|
+
* label; nothing is hand-derived here (the WASM module has no subname PDA
|
|
369
|
+
* derivation and this package never hand-rolls the on-curve check).
|
|
370
|
+
*/
|
|
371
|
+
async function resolveSubname(labelRaw, parentRaw, chain, input) {
|
|
372
|
+
// The parent half must canonicalize as a handle; if it cannot, `@parent`
|
|
373
|
+
// can never be a registered handle → not a subname, try scoped instead.
|
|
374
|
+
let parentCanonical;
|
|
375
|
+
try {
|
|
376
|
+
parentCanonical = normalizeHandle(parentRaw);
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
// The subname label must also canonicalize as a handle label (same rules).
|
|
382
|
+
// If it cannot we cannot build a cache key yet — defer the verdict until
|
|
383
|
+
// after the parent-registration check decides the branch.
|
|
384
|
+
let subLabel = null;
|
|
385
|
+
try {
|
|
386
|
+
subLabel = normalizeHandle(labelRaw);
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
subLabel = null;
|
|
390
|
+
}
|
|
391
|
+
if (subLabel !== null && ttl > 0) {
|
|
392
|
+
const hit = cache.get(`subname:${parentCanonical}:${subLabel}:${chain}`);
|
|
393
|
+
if (hit && hit.expires > Date.now())
|
|
394
|
+
return hit.value;
|
|
395
|
+
}
|
|
396
|
+
// Is `@parent` a REGISTERED handle? That — and only that — decides whether
|
|
397
|
+
// `x.y` is a subname. `not-found`/`invalid-handle` → not a subname, fall
|
|
398
|
+
// through to scoped; a real rpc-error/malformed account propagates.
|
|
399
|
+
let parent;
|
|
400
|
+
try {
|
|
401
|
+
parent = await fetchHandle(parentCanonical, input);
|
|
402
|
+
}
|
|
403
|
+
catch (e) {
|
|
404
|
+
if (e instanceof ResolveError && (e.code === "not-found" || e.code === "invalid-handle")) {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
throw e;
|
|
408
|
+
}
|
|
409
|
+
// COMMITTED to the subname interpretation: `@parent` is a live handle.
|
|
410
|
+
if (subLabel === null) {
|
|
411
|
+
throw new ResolveError("not-found", `"${input.trim()}" is not a subname of @${parentCanonical}`, input);
|
|
412
|
+
}
|
|
413
|
+
const display = `${subLabel}.${parentCanonical}`;
|
|
414
|
+
// Locate the subname account via the program scan (ownership check), then
|
|
415
|
+
// match the canonical label. `fetchSubnames` already flags each with
|
|
416
|
+
// `live` (staleness layer 1: createdAt >= parent.registeredAt).
|
|
417
|
+
const subs = await fetchSubnames(rpc, programBase58, parent.pda, parent.handle.registeredAt);
|
|
418
|
+
const match = subs.find((s) => s.label === subLabel);
|
|
419
|
+
if (!match) {
|
|
420
|
+
throw new ResolveError("not-found", `${display} is not a subname of @${parentCanonical}`, input);
|
|
421
|
+
}
|
|
422
|
+
// Staleness layer 1 — a subname stranded by a parent transfer/re-registration
|
|
423
|
+
// belongs to a PREVIOUS owner and must NEVER resolve to a payment address.
|
|
424
|
+
if (!match.live) {
|
|
425
|
+
throw new ResolveError("not-found", `${display} was created before the current @${parentCanonical} registration — it belongs to a previous owner`, input);
|
|
426
|
+
}
|
|
427
|
+
// Read the subname's LIVE records. `resolveSubnameRecords` re-applies layer
|
|
428
|
+
// 1 (returns [] when stale) AND layer 2 (record.updatedAt >= createdAt),
|
|
429
|
+
// dropping any stale record — the only safe entry point.
|
|
430
|
+
const subname = {
|
|
431
|
+
parent: match.parent,
|
|
432
|
+
label: match.label,
|
|
433
|
+
createdAt: match.createdAt,
|
|
434
|
+
bump: match.bump,
|
|
435
|
+
};
|
|
436
|
+
const records = await resolveSubnameRecords(rpc, programBase58, match.account, subname, parent.handle.registeredAt);
|
|
437
|
+
const record = records.find((r) => !r.stale && r.coinType === CHAIN_COIN_TYPE[chain]);
|
|
438
|
+
if (!record) {
|
|
439
|
+
throw new ResolveError("no-record-for-chain", `${display} has no ${chain} record`, input);
|
|
440
|
+
}
|
|
441
|
+
const value = {
|
|
442
|
+
input,
|
|
443
|
+
name: display,
|
|
444
|
+
// A subname is its own namespace — never "handle" (so a consumer that
|
|
445
|
+
// branches on `=== "handle"`, e.g. dev-api's attestation enrichment, does
|
|
446
|
+
// not treat it as a top-level handle) and never the parent's.
|
|
447
|
+
namespace: "subname",
|
|
448
|
+
address: record.address,
|
|
449
|
+
chain,
|
|
450
|
+
// Like a handle's ETH/BTC record: `unverified` unless the record itself
|
|
451
|
+
// is co-signed/verified (and `verified` is already forced false for any
|
|
452
|
+
// stale record by records.ts).
|
|
453
|
+
verification: record.verified ? "verified" : "unverified",
|
|
454
|
+
};
|
|
455
|
+
if (ttl > 0) {
|
|
456
|
+
cache.set(`subname:${parentCanonical}:${subLabel}:${chain}`, {
|
|
457
|
+
value,
|
|
458
|
+
expires: Date.now() + ttl,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
return value;
|
|
462
|
+
}
|
|
293
463
|
/** See `Resolver.records`. */
|
|
294
464
|
async function records(input) {
|
|
295
465
|
const parsed = parseName(input); // throws with a specific code
|
|
@@ -307,12 +477,22 @@ export function createResolver(config) {
|
|
|
307
477
|
}
|
|
308
478
|
catch (e) {
|
|
309
479
|
// A dotted, non-`@`, non-X1NS name is `unrecognized` to the strict native
|
|
310
|
-
// parser — but it MAY be a
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
//
|
|
480
|
+
// parser — but it MAY be a SUBNAME `label.parent` (#7375) or a scoped
|
|
481
|
+
// customer-TLD name `name.tld` (#8484/#8485). Owner-approved
|
|
482
|
+
// disambiguation order: try the subname interpretation FIRST (when
|
|
483
|
+
// `@parent` is a registered handle), then the scoped path. Each re-throws
|
|
484
|
+
// / falls through so `.sol`/`.eth`/unknown TLDs behave exactly as before.
|
|
485
|
+
// Any other code (ambiguous / invalid-*) is preserved verbatim.
|
|
315
486
|
if (e instanceof ResolveError && e.code === "unrecognized") {
|
|
487
|
+
const sub = subnameCandidate(input);
|
|
488
|
+
if (sub) {
|
|
489
|
+
// `null` ⇒ `@parent` is not a registered handle ⇒ not a subname;
|
|
490
|
+
// fall through to the scoped path. A committed subname path either
|
|
491
|
+
// returns a `Resolved` or throws (never reaches scoped).
|
|
492
|
+
const resolved = await resolveSubname(sub.label, sub.parent, chain, input);
|
|
493
|
+
if (resolved !== null)
|
|
494
|
+
return resolved;
|
|
495
|
+
}
|
|
316
496
|
const cand = scopedTldCandidate(input);
|
|
317
497
|
if (cand)
|
|
318
498
|
return resolveScoped(cand.sub, cand.tld, chain, input);
|
|
@@ -402,8 +582,15 @@ export function createResolver(config) {
|
|
|
402
582
|
*/
|
|
403
583
|
async function reverse(address) {
|
|
404
584
|
const owner = decodeBase58_32(address);
|
|
405
|
-
|
|
406
|
-
|
|
585
|
+
// `decodeBase58_32` left-pads a short/tiny base58 input into 32 bytes, so a
|
|
586
|
+
// malformed address like "abc" would otherwise slip through as a valid tiny
|
|
587
|
+
// key. Require the canonical round-trip: a malformed address must THROW
|
|
588
|
+
// (matching the dev-api's 400 INVALID_ADDRESS and the docs/drill checklist),
|
|
589
|
+
// reserving the `null` return exclusively for a WELL-FORMED address that
|
|
590
|
+
// genuinely has no primary.
|
|
591
|
+
if (!owner || encodeBase58(owner) !== address) {
|
|
592
|
+
throw new ResolveError("unrecognized", `"${address}" is not a valid X1 address`, address);
|
|
593
|
+
}
|
|
407
594
|
const handleName = await reverseHandle(owner);
|
|
408
595
|
if (handleName !== null)
|
|
409
596
|
return handleName;
|
package/dist/signin.d.ts
CHANGED
|
@@ -211,9 +211,21 @@ export type SignInVerification = {
|
|
|
211
211
|
* wallet signed. Whether that wallet currently owns a handle is a separate,
|
|
212
212
|
* on-chain question answered by {@link resolveIdentity}.
|
|
213
213
|
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
214
|
+
* ⚠️ `expectedDomain` and `expectedNonce` are the anti-phishing and anti-replay
|
|
215
|
+
* checks, and they are OPT-IN. OMITTING THEM DISABLES THAT PROTECTION:
|
|
216
|
+
*
|
|
217
|
+
* - no `expectedDomain` → the domain binding is NOT enforced. A signature a
|
|
218
|
+
* user produced for a phishing site's challenge will verify here. Always pass
|
|
219
|
+
* your own origin's domain in a real login.
|
|
220
|
+
* - no `expectedNonce` → the nonce binding is NOT enforced, so this call cannot
|
|
221
|
+
* tell a fresh proof from a replayed one. Pass the nonce the RP issued, and
|
|
222
|
+
* never accept the same nonce twice (burn it server-side). Keep the expiry
|
|
223
|
+
* window short regardless — expiry alone only bounds the replay window, it
|
|
224
|
+
* does not close it.
|
|
225
|
+
*
|
|
226
|
+
* {@link signInWithX1ID} passes both for you. A bare `verifySignIn` without
|
|
227
|
+
* them emits a one-time dev-mode `console.warn` so the gap is noticed in
|
|
228
|
+
* development; it is silent in production (and whenever `process` is absent).
|
|
217
229
|
*/
|
|
218
230
|
export declare function verifySignIn(params: VerifySignInParams): Promise<SignInVerification>;
|
|
219
231
|
/** One live "verified by [platform]" signal on the signed-in handle. */
|
package/dist/signin.js
CHANGED
|
Binary file
|