@x1id/resolve 0.10.0 → 0.12.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 +74 -5
- package/dist/adminConfig.d.ts +63 -0
- package/dist/adminConfig.js +113 -0
- package/dist/adminMarket.d.ts +67 -0
- package/dist/adminMarket.js +134 -0
- package/dist/attestation.d.ts +103 -1
- package/dist/attestation.js +124 -0
- package/dist/gateClient.d.ts +98 -0
- package/dist/gateClient.js +118 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +138 -2
- package/dist/lease.d.ts +92 -0
- package/dist/lease.js +117 -0
- package/dist/namespaceOverride.d.ts +123 -0
- package/dist/namespaceOverride.js +204 -0
- package/dist/register.d.ts +30 -0
- package/dist/register.js +37 -0
- package/dist/scoped.d.ts +84 -0
- package/dist/scoped.js +126 -0
- package/dist/types.d.ts +23 -4
- package/dist/types.js +2 -1
- package/dist/universal.d.ts +58 -0
- package/dist/universal.js +67 -0
- package/dist/universalDetect.d.ts +44 -0
- package/dist/universalDetect.js +105 -0
- package/dist/universalEns.d.ts +44 -0
- package/dist/universalEns.js +138 -0
- package/dist/universalNative.d.ts +13 -0
- package/dist/universalNative.js +43 -0
- package/dist/universalSns.d.ts +95 -0
- package/dist/universalSns.js +223 -0
- package/dist/universalTypes.d.ts +117 -0
- package/dist/universalTypes.js +24 -0
- package/dist/voucher.d.ts +53 -0
- package/dist/voucher.js +56 -0
- package/dist/wasm.d.ts +17 -0
- package/dist/wasm.js +38 -0
- package/package.json +1 -1
- package/wasm/x1_resolve_wasm.wasm +0 -0
package/dist/attestation.js
CHANGED
|
@@ -90,6 +90,14 @@ export const ATTESTATION_KIND_TELEGRAM = 5;
|
|
|
90
90
|
* `>=` this on-chain; the SDK's builder enforces the same bound. Mirrors
|
|
91
91
|
* `Attestation::KIND_COUNT` (programs/x1-handles/src/state.rs). */
|
|
92
92
|
export const ATTESTATION_KIND_COUNT = 6;
|
|
93
|
+
/** `Attestation.kind` — validator control proven TRUSTLESSLY on-chain (#8486):
|
|
94
|
+
* the vote account's `authorized_withdrawer` co-signed `attest_validator`, or
|
|
95
|
+
* signed the challenge that `attest_validator_signed` verifies via the Ed25519
|
|
96
|
+
* precompile. Value 6 — equal to {@link ATTESTATION_KIND_COUNT}, i.e. OUTSIDE
|
|
97
|
+
* the attestor-mintable range (`create_attestation` refuses `>= KIND_COUNT`),
|
|
98
|
+
* so only the cryptographic validator path can mint it. `evidenceHash` holds
|
|
99
|
+
* the vote account pubkey; surfaced as a gold shield, not the blue tick. */
|
|
100
|
+
export const ATTESTATION_KIND_VALIDATOR = 6;
|
|
93
101
|
/** Anchor account discriminator: `sha256("account:Attestation")[0..8]`.
|
|
94
102
|
* Pinned (this SDK is zero-dependency and cannot assume WebCrypto SHA-256
|
|
95
103
|
* everywhere it runs); asserted against a re-derivation in the test suite
|
|
@@ -113,6 +121,22 @@ export const CREATE_ATTESTATION_DISCRIMINATOR = Uint8Array.from([
|
|
|
113
121
|
export const CLOSE_ATTESTATION_DISCRIMINATOR = Uint8Array.from([
|
|
114
122
|
249, 84, 133, 23, 48, 175, 252, 221,
|
|
115
123
|
]);
|
|
124
|
+
/** Anchor instruction discriminator: `sha256("global:attest_validator")[0..8]` — the co-sign path. */
|
|
125
|
+
export const ATTEST_VALIDATOR_DISCRIMINATOR = Uint8Array.from([
|
|
126
|
+
170, 159, 195, 248, 59, 130, 28, 168,
|
|
127
|
+
]);
|
|
128
|
+
/** Anchor instruction discriminator: `sha256("global:attest_validator_signed")[0..8]` — the detached path. */
|
|
129
|
+
export const ATTEST_VALIDATOR_SIGNED_DISCRIMINATOR = Uint8Array.from([
|
|
130
|
+
160, 18, 26, 117, 233, 252, 230, 221,
|
|
131
|
+
]);
|
|
132
|
+
/** The SVM Ed25519 signature-verification precompile — carries the withdraw
|
|
133
|
+
* authority's detached signature for `attest_validator_signed`. */
|
|
134
|
+
export const ED25519_PROGRAM_ID = "Ed25519SigVerify111111111111111111111111111";
|
|
135
|
+
/** The Instructions sysvar — `attest_validator_signed` introspects it. */
|
|
136
|
+
export const SYSVAR_INSTRUCTIONS_ID = "Sysvar1nstructions1111111111111111111111111";
|
|
137
|
+
/** Domain-separator prefix of the validator-attestation challenge. MUST byte-
|
|
138
|
+
* match `VALIDATOR_ATTEST_CHALLENGE_PREFIX` in programs/x1-handles/src/lib.rs. */
|
|
139
|
+
export const VALIDATOR_ATTEST_CHALLENGE_PREFIX = new TextEncoder().encode("x1id:validator-attest:v1");
|
|
116
140
|
/** `Attestation` account size — every field is fixed-width, so unlike a
|
|
117
141
|
* `Handle` the account is exactly this long:
|
|
118
142
|
* disc(8) + handle(32) + kind(1) + evidence_hash(32) + attested_at(8)
|
|
@@ -157,6 +181,8 @@ export function attestationKindName(kind) {
|
|
|
157
181
|
return "github";
|
|
158
182
|
case ATTESTATION_KIND_TELEGRAM:
|
|
159
183
|
return "telegram";
|
|
184
|
+
case ATTESTATION_KIND_VALIDATOR:
|
|
185
|
+
return "validator";
|
|
160
186
|
default:
|
|
161
187
|
return null;
|
|
162
188
|
}
|
|
@@ -177,6 +203,8 @@ export function platformSlug(kind) {
|
|
|
177
203
|
return "github";
|
|
178
204
|
case ATTESTATION_KIND_TELEGRAM:
|
|
179
205
|
return "telegram";
|
|
206
|
+
case ATTESTATION_KIND_VALIDATOR:
|
|
207
|
+
return "validator";
|
|
180
208
|
default:
|
|
181
209
|
return null;
|
|
182
210
|
}
|
|
@@ -365,6 +393,102 @@ export function buildCloseAttestationIx(p) {
|
|
|
365
393
|
data: CLOSE_ATTESTATION_DISCRIMINATOR.slice(),
|
|
366
394
|
};
|
|
367
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Build `attest_validator` (co-sign path). Two signers — `owner` (authorizes
|
|
398
|
+
* the link) and `withdrawAuthority` (the proof) — which MAY be the same key. No
|
|
399
|
+
* instruction args: the co-signature IS the proof.
|
|
400
|
+
*/
|
|
401
|
+
export function buildAttestValidatorIx(p) {
|
|
402
|
+
const keys = [
|
|
403
|
+
{ pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: true },
|
|
404
|
+
{ pubkey: toBase58(p.withdrawAuthority, "withdrawAuthority"), isSigner: true, isWritable: false },
|
|
405
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: false },
|
|
406
|
+
{ pubkey: toBase58(p.voteAccount, "voteAccount"), isSigner: false, isWritable: false },
|
|
407
|
+
{ pubkey: toBase58(p.attestation, "attestation"), isSigner: false, isWritable: true },
|
|
408
|
+
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
|
|
409
|
+
];
|
|
410
|
+
if (p.holderTokenAccount !== undefined) {
|
|
411
|
+
keys.push({ pubkey: toBase58(p.holderTokenAccount, "holderTokenAccount"), isSigner: false, isWritable: false });
|
|
412
|
+
}
|
|
413
|
+
return { programId: toBase58(p.programId, "programId"), keys, data: ATTEST_VALIDATOR_DISCRIMINATOR.slice() };
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* The exact 96-byte challenge the withdraw authority signs for the DETACHED
|
|
417
|
+
* path: `PREFIX(24) ‖ handle(32) ‖ voteAccount(32) ‖ registeredAt i64 LE(8)`.
|
|
418
|
+
* MUST byte-match the program. `registeredAt` is the handle's on-chain
|
|
419
|
+
* `registered_at` (seconds) — pass the exact value from the Handle account.
|
|
420
|
+
*/
|
|
421
|
+
export function validatorAttestChallenge(handle, voteAccount, registeredAt) {
|
|
422
|
+
const out = new Uint8Array(24 + 32 + 32 + 8);
|
|
423
|
+
out.set(VALIDATOR_ATTEST_CHALLENGE_PREFIX, 0);
|
|
424
|
+
out.set(toBytes32(handle, "handle"), 24);
|
|
425
|
+
out.set(toBytes32(voteAccount, "voteAccount"), 56);
|
|
426
|
+
new DataView(out.buffer, out.byteOffset, out.byteLength).setBigInt64(88, BigInt(registeredAt), true);
|
|
427
|
+
return out;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Build the Ed25519 precompile instruction (no accounts) proving `signature` is
|
|
431
|
+
* `publicKey`'s over `message`. SELF-CONTAINED single signature — all three
|
|
432
|
+
* instruction-index fields are `0xFFFF` — matching the exact layout
|
|
433
|
+
* `attest_validator_signed` requires. Place it IMMEDIATELY BEFORE the attest ix
|
|
434
|
+
* (use {@link buildAttestValidatorSignedIxs}).
|
|
435
|
+
*/
|
|
436
|
+
export function buildEd25519VerifyIx(p) {
|
|
437
|
+
const pk = toBytes32(p.publicKey, "publicKey");
|
|
438
|
+
if (p.signature.length !== 64)
|
|
439
|
+
throw new Error("signature must be exactly 64 bytes");
|
|
440
|
+
const SELF = 0xffff;
|
|
441
|
+
const PK_OFF = 16;
|
|
442
|
+
const SIG_OFF = 48;
|
|
443
|
+
const MSG_OFF = 112;
|
|
444
|
+
const data = new Uint8Array(MSG_OFF + p.message.length);
|
|
445
|
+
data[0] = 1; // num_signatures
|
|
446
|
+
data[1] = 0; // padding
|
|
447
|
+
const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
448
|
+
dv.setUint16(2, SIG_OFF, true);
|
|
449
|
+
dv.setUint16(4, SELF, true); // signature_instruction_index
|
|
450
|
+
dv.setUint16(6, PK_OFF, true);
|
|
451
|
+
dv.setUint16(8, SELF, true); // public_key_instruction_index
|
|
452
|
+
dv.setUint16(10, MSG_OFF, true);
|
|
453
|
+
dv.setUint16(12, p.message.length, true); // message_data_size
|
|
454
|
+
dv.setUint16(14, SELF, true); // message_instruction_index
|
|
455
|
+
data.set(pk, PK_OFF);
|
|
456
|
+
data.set(p.signature, SIG_OFF);
|
|
457
|
+
data.set(p.message, MSG_OFF);
|
|
458
|
+
return { programId: ED25519_PROGRAM_ID, keys: [], data };
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Build the `attest_validator_signed` ix ALONE. The withdraw authority's proof
|
|
462
|
+
* is NOT here — it rides in the Ed25519 precompile ix that MUST immediately
|
|
463
|
+
* precede this one. Prefer {@link buildAttestValidatorSignedIxs}, which returns
|
|
464
|
+
* both, in order. No instruction args.
|
|
465
|
+
*/
|
|
466
|
+
export function buildAttestValidatorSignedIx(p) {
|
|
467
|
+
const keys = [
|
|
468
|
+
{ pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: true },
|
|
469
|
+
{ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: false },
|
|
470
|
+
{ pubkey: toBase58(p.voteAccount, "voteAccount"), isSigner: false, isWritable: false },
|
|
471
|
+
{ pubkey: toBase58(p.attestation, "attestation"), isSigner: false, isWritable: true },
|
|
472
|
+
{ pubkey: SYSVAR_INSTRUCTIONS_ID, isSigner: false, isWritable: false },
|
|
473
|
+
{ pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
|
|
474
|
+
];
|
|
475
|
+
if (p.holderTokenAccount !== undefined) {
|
|
476
|
+
keys.push({ pubkey: toBase58(p.holderTokenAccount, "holderTokenAccount"), isSigner: false, isWritable: false });
|
|
477
|
+
}
|
|
478
|
+
return { programId: toBase58(p.programId, "programId"), keys, data: ATTEST_VALIDATOR_SIGNED_DISCRIMINATOR.slice() };
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Build BOTH instructions for the detached path, IN ORDER:
|
|
482
|
+
* `[ed25519VerifyIx, attestValidatorSignedIx]`. Submit them as ONE transaction
|
|
483
|
+
* in this order — the program loads the ed25519 ix at `current_index - 1`.
|
|
484
|
+
* `owner` is the only signer.
|
|
485
|
+
*/
|
|
486
|
+
export function buildAttestValidatorSignedIxs(p) {
|
|
487
|
+
const message = validatorAttestChallenge(p.handle, p.voteAccount, p.registeredAt);
|
|
488
|
+
const ed = buildEd25519VerifyIx({ publicKey: p.withdrawAuthority, message, signature: p.signature });
|
|
489
|
+
const attest = buildAttestValidatorSignedIx(p);
|
|
490
|
+
return [ed, attest];
|
|
491
|
+
}
|
|
368
492
|
/**
|
|
369
493
|
* The platform slugs a handle is LIVE-verified on — e.g. `["x", "github"]` —
|
|
370
494
|
* de-duplicated, in first-seen order. Only entries with `verified === true`
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate middleware — the drop-in "gate this route" client for X1ID Gate
|
|
3
|
+
* (gating-as-a-service, #8453). A tiny, framework-agnostic helper that calls the
|
|
4
|
+
* hosted `POST /v1/gate/check` decision endpoint (api.x1id.io) so a dapp does not
|
|
5
|
+
* run its own attestation reads. Pairs with the off-chain {@link Gate} /
|
|
6
|
+
* {@link GateKind} this package already exports.
|
|
7
|
+
*
|
|
8
|
+
* Zero-dependency (uses `fetch`); runs anywhere a server runtime has `fetch`
|
|
9
|
+
* (Node 18+, Next route handlers, edge, Workers). It is a SERVER helper — your
|
|
10
|
+
* X1ID API key must never ship to a browser.
|
|
11
|
+
*
|
|
12
|
+
* Trust: a `passed: true` means the handle carries a live, x1id-attestor-signed
|
|
13
|
+
* attestation matching the policy — "verified by x1id", NOT trustless proof and
|
|
14
|
+
* NOT legal personhood. Gate on it accordingly.
|
|
15
|
+
*/
|
|
16
|
+
import type { Gate } from "./gate.js";
|
|
17
|
+
/** Default hosted Gate base URL. Override for staging / self-host. */
|
|
18
|
+
export declare const DEFAULT_GATE_BASE_URL = "https://api.x1id.io";
|
|
19
|
+
export interface GateClientConfig {
|
|
20
|
+
/** Your X1ID API key (`x1id_…` / `x1idlive_…`). SERVER-ONLY — never expose it. */
|
|
21
|
+
readonly apiKey: string;
|
|
22
|
+
/** Base URL of the /v1 API. Defaults to {@link DEFAULT_GATE_BASE_URL}. */
|
|
23
|
+
readonly baseUrl?: string;
|
|
24
|
+
/** Inject a `fetch` (tests, non-global-fetch runtimes). */
|
|
25
|
+
readonly fetchImpl?: typeof fetch;
|
|
26
|
+
/** Per-request timeout in ms (default 10_000). */
|
|
27
|
+
readonly timeoutMs?: number;
|
|
28
|
+
}
|
|
29
|
+
/** Exactly one of `handle` / `wallet`. */
|
|
30
|
+
export type GateSubject = {
|
|
31
|
+
readonly handle: string;
|
|
32
|
+
readonly wallet?: never;
|
|
33
|
+
} | {
|
|
34
|
+
readonly wallet: string;
|
|
35
|
+
readonly handle?: never;
|
|
36
|
+
};
|
|
37
|
+
/** A policy to evaluate: an inline {@link Gate}, or a stored policy id (`gp_…`). */
|
|
38
|
+
export type GatePolicy = Gate | string;
|
|
39
|
+
export interface GateAttestationView {
|
|
40
|
+
readonly platform: string;
|
|
41
|
+
readonly kind: number;
|
|
42
|
+
readonly attested_at: number;
|
|
43
|
+
}
|
|
44
|
+
/** The decision the service returned. */
|
|
45
|
+
export interface GateCheckResult {
|
|
46
|
+
readonly passed: boolean;
|
|
47
|
+
readonly subject: {
|
|
48
|
+
readonly handle: string | null;
|
|
49
|
+
readonly wallet: string | null;
|
|
50
|
+
};
|
|
51
|
+
readonly policy: Gate;
|
|
52
|
+
readonly attestations: readonly GateAttestationView[];
|
|
53
|
+
readonly reason?: "no-primary-handle" | "handle-not-registered";
|
|
54
|
+
}
|
|
55
|
+
/** Thrown when the decision could not be obtained (network, auth, 5xx, bad
|
|
56
|
+
* input). NOT thrown for a clean `passed: false` — that is a valid decision. */
|
|
57
|
+
export declare class GateError extends Error {
|
|
58
|
+
readonly code: string;
|
|
59
|
+
readonly status?: number | undefined;
|
|
60
|
+
constructor(code: string, message: string, status?: number | undefined);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Low-level: ask the hosted service whether `subject` passes `policy`. Resolves
|
|
64
|
+
* to the decision (including a clean `passed: false`); throws {@link GateError}
|
|
65
|
+
* only when no decision could be obtained.
|
|
66
|
+
*/
|
|
67
|
+
export declare function checkGate(cfg: GateClientConfig, subject: GateSubject, policy: GatePolicy): Promise<GateCheckResult>;
|
|
68
|
+
export interface GuardResult {
|
|
69
|
+
/** Whether the subject passes the policy. */
|
|
70
|
+
readonly allowed: boolean;
|
|
71
|
+
/** The full decision (attestations, reason, …). */
|
|
72
|
+
readonly decision: GateCheckResult;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Build a reusable guard for one policy. The returned function takes a subject
|
|
76
|
+
* and resolves to `{ allowed, decision }`.
|
|
77
|
+
*
|
|
78
|
+
* Fail-closed by default: if the decision cannot be obtained (network/5xx), the
|
|
79
|
+
* guard RE-THROWS {@link GateError} so your route returns an error rather than
|
|
80
|
+
* silently admitting an unverified caller. Pass `onError: "allow"` to fail-open.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* import { requireGate, GateKind } from "@x1id/resolve";
|
|
85
|
+
* const gate = requireGate({ apiKey: process.env.X1ID_API_KEY! }, { kind: GateKind.github });
|
|
86
|
+
*
|
|
87
|
+
* // Next.js Route Handler
|
|
88
|
+
* export async function POST(req: Request) {
|
|
89
|
+
* const { handle } = await req.json();
|
|
90
|
+
* const { allowed } = await gate({ handle });
|
|
91
|
+
* if (!allowed) return Response.json({ error: "verified GitHub required" }, { status: 403 });
|
|
92
|
+
* // …proceed
|
|
93
|
+
* }
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export declare function requireGate(cfg: GateClientConfig, policy: GatePolicy, opts?: {
|
|
97
|
+
readonly onError?: "throw" | "allow" | "deny";
|
|
98
|
+
}): (subject: GateSubject) => Promise<GuardResult>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate middleware — the drop-in "gate this route" client for X1ID Gate
|
|
3
|
+
* (gating-as-a-service, #8453). A tiny, framework-agnostic helper that calls the
|
|
4
|
+
* hosted `POST /v1/gate/check` decision endpoint (api.x1id.io) so a dapp does not
|
|
5
|
+
* run its own attestation reads. Pairs with the off-chain {@link Gate} /
|
|
6
|
+
* {@link GateKind} this package already exports.
|
|
7
|
+
*
|
|
8
|
+
* Zero-dependency (uses `fetch`); runs anywhere a server runtime has `fetch`
|
|
9
|
+
* (Node 18+, Next route handlers, edge, Workers). It is a SERVER helper — your
|
|
10
|
+
* X1ID API key must never ship to a browser.
|
|
11
|
+
*
|
|
12
|
+
* Trust: a `passed: true` means the handle carries a live, x1id-attestor-signed
|
|
13
|
+
* attestation matching the policy — "verified by x1id", NOT trustless proof and
|
|
14
|
+
* NOT legal personhood. Gate on it accordingly.
|
|
15
|
+
*/
|
|
16
|
+
/** Default hosted Gate base URL. Override for staging / self-host. */
|
|
17
|
+
export const DEFAULT_GATE_BASE_URL = "https://api.x1id.io";
|
|
18
|
+
/** Thrown when the decision could not be obtained (network, auth, 5xx, bad
|
|
19
|
+
* input). NOT thrown for a clean `passed: false` — that is a valid decision. */
|
|
20
|
+
export class GateError extends Error {
|
|
21
|
+
code;
|
|
22
|
+
status;
|
|
23
|
+
constructor(code, message, status) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.status = status;
|
|
27
|
+
this.name = "GateError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function baseUrlOf(cfg) {
|
|
31
|
+
return (cfg.baseUrl ?? DEFAULT_GATE_BASE_URL).replace(/\/+$/, "");
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Low-level: ask the hosted service whether `subject` passes `policy`. Resolves
|
|
35
|
+
* to the decision (including a clean `passed: false`); throws {@link GateError}
|
|
36
|
+
* only when no decision could be obtained.
|
|
37
|
+
*/
|
|
38
|
+
export async function checkGate(cfg, subject, policy) {
|
|
39
|
+
const doFetch = cfg.fetchImpl ?? globalThis.fetch;
|
|
40
|
+
if (typeof doFetch !== "function")
|
|
41
|
+
throw new GateError("NO_FETCH", "no fetch available; pass fetchImpl in the config");
|
|
42
|
+
if (!cfg.apiKey)
|
|
43
|
+
throw new GateError("NO_API_KEY", "an X1ID apiKey is required");
|
|
44
|
+
const url = `${baseUrlOf(cfg)}/v1/gate/check`;
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const timer = setTimeout(() => controller.abort(), cfg.timeoutMs ?? 10_000);
|
|
47
|
+
let res;
|
|
48
|
+
try {
|
|
49
|
+
res = await doFetch(url, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "content-type": "application/json", "x-api-key": cfg.apiKey },
|
|
52
|
+
body: JSON.stringify({ ...subject, policy }),
|
|
53
|
+
signal: controller.signal,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
throw new GateError("REQUEST_FAILED", `gate request failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
}
|
|
62
|
+
let body;
|
|
63
|
+
try {
|
|
64
|
+
body = await res.json();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new GateError("BAD_RESPONSE", `gate returned non-JSON (HTTP ${res.status})`, res.status);
|
|
68
|
+
}
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
const b = (body ?? {});
|
|
71
|
+
throw new GateError(b.code ?? "GATE_HTTP_ERROR", b.message ?? `gate returned HTTP ${res.status}`, res.status);
|
|
72
|
+
}
|
|
73
|
+
return body;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Build a reusable guard for one policy. The returned function takes a subject
|
|
77
|
+
* and resolves to `{ allowed, decision }`.
|
|
78
|
+
*
|
|
79
|
+
* Fail-closed by default: if the decision cannot be obtained (network/5xx), the
|
|
80
|
+
* guard RE-THROWS {@link GateError} so your route returns an error rather than
|
|
81
|
+
* silently admitting an unverified caller. Pass `onError: "allow"` to fail-open.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* import { requireGate, GateKind } from "@x1id/resolve";
|
|
86
|
+
* const gate = requireGate({ apiKey: process.env.X1ID_API_KEY! }, { kind: GateKind.github });
|
|
87
|
+
*
|
|
88
|
+
* // Next.js Route Handler
|
|
89
|
+
* export async function POST(req: Request) {
|
|
90
|
+
* const { handle } = await req.json();
|
|
91
|
+
* const { allowed } = await gate({ handle });
|
|
92
|
+
* if (!allowed) return Response.json({ error: "verified GitHub required" }, { status: 403 });
|
|
93
|
+
* // …proceed
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export function requireGate(cfg, policy, opts = {}) {
|
|
98
|
+
const onError = opts.onError ?? "throw";
|
|
99
|
+
return async (subject) => {
|
|
100
|
+
try {
|
|
101
|
+
const decision = await checkGate(cfg, subject, policy);
|
|
102
|
+
return { allowed: decision.passed, decision };
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
if (onError === "throw")
|
|
106
|
+
throw e;
|
|
107
|
+
const handle = "handle" in subject && subject.handle ? subject.handle : null;
|
|
108
|
+
const wallet = "wallet" in subject && subject.wallet ? subject.wallet : null;
|
|
109
|
+
const decision = {
|
|
110
|
+
passed: onError === "allow",
|
|
111
|
+
subject: { handle, wallet },
|
|
112
|
+
policy: policy,
|
|
113
|
+
attestations: [],
|
|
114
|
+
};
|
|
115
|
+
return { allowed: onError === "allow", decision };
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -35,12 +35,15 @@ export * from "./subname.js";
|
|
|
35
35
|
export * from "./recordCount.js";
|
|
36
36
|
export * from "./attestation.js";
|
|
37
37
|
export * from "./gate.js";
|
|
38
|
+
export * from "./gateClient.js";
|
|
38
39
|
export * from "./textRecords.js";
|
|
39
40
|
export * from "./lock.js";
|
|
40
41
|
export * from "./integrator.js";
|
|
41
42
|
export * from "./voucher.js";
|
|
42
43
|
export * from "./agent.js";
|
|
43
44
|
export * from "./register.js";
|
|
45
|
+
export * from "./adminConfig.js";
|
|
46
|
+
export * from "./lease.js";
|
|
44
47
|
export * from "./accounts.js";
|
|
45
48
|
export * from "./x402.js";
|
|
46
49
|
export * from "./recordWrite.js";
|
|
@@ -49,7 +52,9 @@ export * from "./commitReveal.js";
|
|
|
49
52
|
export * from "./pnftTransfer.js";
|
|
50
53
|
export * from "./signin.js";
|
|
51
54
|
export * from "./domainProof.js";
|
|
55
|
+
export { scopedTldCandidate, namespaceIsActive, makeScopedHandleDeriver, type ScopedCandidate, type ScopedHandleDeriver, } from "./scoped.js";
|
|
52
56
|
import { type Chain, type Resolved } from "./types.js";
|
|
57
|
+
import { type ScopedHandleDeriver } from "./scoped.js";
|
|
53
58
|
import { WasmResolver } from "./wasm.js";
|
|
54
59
|
import { type HandleRecord } from "./records.js";
|
|
55
60
|
export interface ResolverConfig {
|
|
@@ -68,6 +73,18 @@ export interface ResolverConfig {
|
|
|
68
73
|
readonly cacheTtlMs?: number;
|
|
69
74
|
/** @handle registry program id. Defaults to the canonical X1 deployment. */
|
|
70
75
|
readonly handleProgramId?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Deriver for a SCOPED customer-TLD name's `["handle", tld, name]` PDA
|
|
78
|
+
* (`alices.testtld`, #8484/#8485). Optional and only used for scoped names:
|
|
79
|
+
* `@handle` and X1NS resolution never touch it. The WASM module has no
|
|
80
|
+
* two-seed scoped-handle derivation and this package never hand-rolls the
|
|
81
|
+
* on-curve check (see scoped.ts), so a scoped name is resolvable only when a
|
|
82
|
+
* deriver is injected — build the default with
|
|
83
|
+
* {@link makeScopedHandleDeriver} (needs `@solana/web3.js`), or pass a mock.
|
|
84
|
+
* Absent → a scoped name whose `.tld` IS a launched namespace throws
|
|
85
|
+
* `not-configured`; unlaunched `.tld`s stay `unrecognized`, unchanged.
|
|
86
|
+
*/
|
|
87
|
+
readonly scopedHandleDeriver?: ScopedHandleDeriver;
|
|
71
88
|
}
|
|
72
89
|
export interface ResolveOptions {
|
|
73
90
|
/** Which chain's address to return. Default "X1". */
|
|
@@ -102,3 +119,6 @@ export interface Resolver {
|
|
|
102
119
|
clearCache(): void;
|
|
103
120
|
}
|
|
104
121
|
export declare function createResolver(config: ResolverConfig): Resolver;
|
|
122
|
+
export * from "./adminMarket.js";
|
|
123
|
+
export * from "./namespaceOverride.js";
|
|
124
|
+
export * from "./universal.js";
|
package/dist/index.js
CHANGED
|
@@ -35,12 +35,15 @@ export * from "./subname.js";
|
|
|
35
35
|
export * from "./recordCount.js";
|
|
36
36
|
export * from "./attestation.js";
|
|
37
37
|
export * from "./gate.js";
|
|
38
|
+
export * from "./gateClient.js";
|
|
38
39
|
export * from "./textRecords.js";
|
|
39
40
|
export * from "./lock.js";
|
|
40
41
|
export * from "./integrator.js";
|
|
41
42
|
export * from "./voucher.js";
|
|
42
43
|
export * from "./agent.js";
|
|
43
44
|
export * from "./register.js";
|
|
45
|
+
export * from "./adminConfig.js";
|
|
46
|
+
export * from "./lease.js";
|
|
44
47
|
// Was previously imported here for internal use only, never re-exported —
|
|
45
48
|
// promoted to public API 2026-09-22 because a second real package (mcp/)
|
|
46
49
|
// now needs `parseHandleAccount`/`RpcFn` directly rather than duplicating
|
|
@@ -56,8 +59,14 @@ export * from "./commitReveal.js";
|
|
|
56
59
|
export * from "./pnftTransfer.js";
|
|
57
60
|
export * from "./signin.js";
|
|
58
61
|
export * from "./domainProof.js";
|
|
62
|
+
// Named (not `export *`): `NAMESPACE_MAX_LEN` is already exported by
|
|
63
|
+
// adminMarket.js, and two `export *` sharing a name would make it ambiguous
|
|
64
|
+
// (dropped from the package's public surface). Re-export the scoped API, minus
|
|
65
|
+
// that one equal-valued constant.
|
|
66
|
+
export { scopedTldCandidate, namespaceIsActive, makeScopedHandleDeriver, } from "./scoped.js";
|
|
59
67
|
import { ResolveError, CHAIN_COIN_TYPE } from "./types.js";
|
|
60
|
-
import { parseName } from "./parse.js";
|
|
68
|
+
import { parseName, normalizeHandle } from "./parse.js";
|
|
69
|
+
import { scopedTldCandidate, namespaceIsActive, NAMESPACE_MAX_LEN, } from "./scoped.js";
|
|
61
70
|
import { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
62
71
|
import { DEFAULT_HANDLE_PROGRAM, TOKEN_ACCOUNT_MIN_LEN, bytesEqual, makeAccountReader, nftHolder, parseHandleAccount, readI64, readU64, } from "./accounts.js";
|
|
63
72
|
import { fetchRecords, liveRecords } from "./records.js";
|
|
@@ -90,6 +99,7 @@ export function createResolver(config) {
|
|
|
90
99
|
// Re-encoded (not the caller's string) so a non-canonical base58 spelling of
|
|
91
100
|
// the same key still compares equal to the RPC's `owner` field.
|
|
92
101
|
const programBase58 = encodeBase58(handleProgram);
|
|
102
|
+
const scopedDeriver = config.scopedHandleDeriver ?? null;
|
|
93
103
|
async function resolveX1ns(canonical, label, tld, chain, input) {
|
|
94
104
|
const account = config.wasm.deriveX1nsAccount(label, tld);
|
|
95
105
|
if (!account) {
|
|
@@ -177,6 +187,109 @@ export function createResolver(config) {
|
|
|
177
187
|
: await nftHolder(reader, config.wasm, canonical, handle.nftMint, input);
|
|
178
188
|
return { input, name: canonical, namespace: "handle", address, chain, verification };
|
|
179
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Resolve a SCOPED customer-TLD name `name.tld` (#8484/#8485), or throw
|
|
192
|
+
* `unrecognized` when `.tld` is not one of OUR launched namespaces — so an
|
|
193
|
+
* unlaunched `.tld` behaves exactly as before (never a scoped hit). The
|
|
194
|
+
* contract mirrors `tools/api`'s `resolve_scoped`: canonicalize both halves
|
|
195
|
+
* as the program does, confirm `["namespace", tld]` is a program-owned Active
|
|
196
|
+
* `Namespace`, then derive `["handle", tld, name]` and read its CURRENT
|
|
197
|
+
* authority through the SAME owner/tokenization/record path a bare `@handle`
|
|
198
|
+
* uses — only the `name`/`namespace` fields differ (the TLD label).
|
|
199
|
+
*/
|
|
200
|
+
async function resolveScoped(subRaw, tldRaw, chain, input) {
|
|
201
|
+
// The exact fall-through the native parser would have produced: a
|
|
202
|
+
// non-canonical half, or a `.tld` that is not a launched namespace, is
|
|
203
|
+
// simply "not a handle or a known domain" — never a misleading
|
|
204
|
+
// invalid-handle, and never a scoped hit on a name we do not own.
|
|
205
|
+
const notScoped = () => new ResolveError("unrecognized", `"${input.trim()}" is not a handle or a known domain`, input);
|
|
206
|
+
// Canonicalize both halves exactly as the program's `handle_normalize`
|
|
207
|
+
// does. A half that does not normalize can never name one of our scoped
|
|
208
|
+
// names — fall through rather than erroring (matches resolve_scoped).
|
|
209
|
+
let name;
|
|
210
|
+
let label;
|
|
211
|
+
try {
|
|
212
|
+
name = normalizeHandle(subRaw);
|
|
213
|
+
label = normalizeHandle(tldRaw);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
throw notScoped();
|
|
217
|
+
}
|
|
218
|
+
if (label.length > NAMESPACE_MAX_LEN)
|
|
219
|
+
throw notScoped();
|
|
220
|
+
if (ttl > 0) {
|
|
221
|
+
const hit = cache.get(`scoped:${label}:${name}:${chain}`);
|
|
222
|
+
if (hit && hit.expires > Date.now())
|
|
223
|
+
return hit.value;
|
|
224
|
+
}
|
|
225
|
+
// Is `.label` a launched, Active X1ID namespace? Derive `["namespace",
|
|
226
|
+
// label]` via the WASM (canonical, always available) and read it. It must
|
|
227
|
+
// exist, be owned by the registry program, and carry `status == Active`.
|
|
228
|
+
const nsAccountKey = config.wasm.deriveNamespaceAccount(label, handleProgram);
|
|
229
|
+
if (!nsAccountKey)
|
|
230
|
+
throw notScoped();
|
|
231
|
+
const ns = await accountInfo(encodeBase58(nsAccountKey));
|
|
232
|
+
if (!ns || ns.owner !== programBase58 || !namespaceIsActive(ns.data)) {
|
|
233
|
+
// Not one of our launched namespaces — preserve today's behavior so an
|
|
234
|
+
// unlaunched `.tld` (and `.sol`/`.eth`, handled elsewhere) stays
|
|
235
|
+
// "unrecognized", never resolved through the wrong path.
|
|
236
|
+
throw notScoped();
|
|
237
|
+
}
|
|
238
|
+
// Confirmed a scoped X1ID name. From here any failure is a REAL error,
|
|
239
|
+
// never a silent fall-through (mirrors resolve_scoped's comment).
|
|
240
|
+
if (!scopedDeriver) {
|
|
241
|
+
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
|
+
}
|
|
243
|
+
let pda;
|
|
244
|
+
try {
|
|
245
|
+
pda = await scopedDeriver.scopedHandleKey(label, name, programBase58);
|
|
246
|
+
}
|
|
247
|
+
catch (e) {
|
|
248
|
+
throw new ResolveError("rpc-error", `scoped handle derivation failed: ${String(e)}`, input);
|
|
249
|
+
}
|
|
250
|
+
const h = await accountInfo(pda);
|
|
251
|
+
// An account at the PDA the registry does not own is not a handle — the
|
|
252
|
+
// scoped name is unregistered under this (live) TLD.
|
|
253
|
+
if (!h || h.owner !== programBase58) {
|
|
254
|
+
throw new ResolveError("not-found", `${name}.${label} is not registered`, input);
|
|
255
|
+
}
|
|
256
|
+
const handle = parseHandleAccount(h.data);
|
|
257
|
+
if (!handle) {
|
|
258
|
+
throw new ResolveError("rpc-error", `${name}.${label} returned a malformed account`, input);
|
|
259
|
+
}
|
|
260
|
+
const display = `${name}.${label}`;
|
|
261
|
+
let value;
|
|
262
|
+
if (chain !== "X1" && chain !== "SOL") {
|
|
263
|
+
// ETH/BTC addresses live in per-chain `Record` accounts — same staleness
|
|
264
|
+
// rule as a bare handle (fetchRecords → liveRecords).
|
|
265
|
+
const live = liveRecords(await fetchRecords(rpc, programBase58, pda, handle.registeredAt, handle.recordsClearedAt));
|
|
266
|
+
const record = live.find((r) => r.coinType === CHAIN_COIN_TYPE[chain]);
|
|
267
|
+
if (!record) {
|
|
268
|
+
throw new ResolveError("no-record-for-chain", `${display} has no ${chain} record`, input);
|
|
269
|
+
}
|
|
270
|
+
value = {
|
|
271
|
+
input,
|
|
272
|
+
name: display,
|
|
273
|
+
namespace: label,
|
|
274
|
+
address: record.address,
|
|
275
|
+
chain,
|
|
276
|
+
verification: record.verified ? "verified" : "unverified",
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
// Same authority rule as a bare handle: untokenized → `Handle.owner`
|
|
281
|
+
// (verified); tokenized → whoever holds the NFT now (verified only in its
|
|
282
|
+
// ATA). A scoped name is always tokenized in practice, but both branches
|
|
283
|
+
// are kept so the shape is byte-identical to resolveHandle.
|
|
284
|
+
const { address, verification } = handle.nftMint === null
|
|
285
|
+
? { address: encodeBase58(handle.owner), verification: "verified" }
|
|
286
|
+
: await nftHolder(reader, config.wasm, display, handle.nftMint, input);
|
|
287
|
+
value = { input, name: display, namespace: label, address, chain, verification };
|
|
288
|
+
}
|
|
289
|
+
if (ttl > 0)
|
|
290
|
+
cache.set(`scoped:${label}:${name}:${chain}`, { value, expires: Date.now() + ttl });
|
|
291
|
+
return value;
|
|
292
|
+
}
|
|
180
293
|
/** See `Resolver.records`. */
|
|
181
294
|
async function records(input) {
|
|
182
295
|
const parsed = parseName(input); // throws with a specific code
|
|
@@ -188,7 +301,24 @@ export function createResolver(config) {
|
|
|
188
301
|
}
|
|
189
302
|
async function resolve(input, opts) {
|
|
190
303
|
const chain = opts?.chain ?? "X1";
|
|
191
|
-
|
|
304
|
+
let parsed;
|
|
305
|
+
try {
|
|
306
|
+
parsed = parseName(input); // throws with a specific code
|
|
307
|
+
}
|
|
308
|
+
catch (e) {
|
|
309
|
+
// A dotted, non-`@`, non-X1NS name is `unrecognized` to the strict native
|
|
310
|
+
// parser — but it MAY be a scoped customer-TLD name `name.tld` (#8484/
|
|
311
|
+
// #8485). Try that before giving up; `resolveScoped` itself re-throws the
|
|
312
|
+
// same `unrecognized` when `.tld` is not a launched namespace, so
|
|
313
|
+
// `.sol`/`.eth`/unknown TLDs behave exactly as before. Any other code
|
|
314
|
+
// (ambiguous / invalid-*) is preserved verbatim.
|
|
315
|
+
if (e instanceof ResolveError && e.code === "unrecognized") {
|
|
316
|
+
const cand = scopedTldCandidate(input);
|
|
317
|
+
if (cand)
|
|
318
|
+
return resolveScoped(cand.sub, cand.tld, chain, input);
|
|
319
|
+
}
|
|
320
|
+
throw e;
|
|
321
|
+
}
|
|
192
322
|
const key = `${parsed.namespace}:${parsed.canonical}:${chain}`;
|
|
193
323
|
if (ttl > 0) {
|
|
194
324
|
const hit = cache.get(key);
|
|
@@ -303,3 +433,9 @@ export function createResolver(config) {
|
|
|
303
433
|
clearCache: () => cache.clear(),
|
|
304
434
|
};
|
|
305
435
|
}
|
|
436
|
+
export * from "./adminMarket.js";
|
|
437
|
+
export * from "./namespaceOverride.js";
|
|
438
|
+
// The X1ID Universal Resolver (#8475): cross-namespace resolution (.sol via SNS,
|
|
439
|
+
// .eth via ENS) alongside the native path. Additive — the native resolver above
|
|
440
|
+
// is untouched; `createUniversalResolver` composes it with external adapters.
|
|
441
|
+
export * from "./universal.js";
|