@x1id/resolve 0.15.0 → 0.15.1
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 +16 -4
- package/dist/gateClient.d.ts +10 -8
- package/dist/gateClient.js +27 -8
- package/dist/signin.js +20 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -366,10 +366,22 @@ export async function POST(req: Request) { // any server runtime with
|
|
|
366
366
|
}
|
|
367
367
|
```
|
|
368
368
|
|
|
369
|
-
Fail-closed by default (re-throws if the decision can't be obtained
|
|
370
|
-
`
|
|
371
|
-
the full decision; pass a stored policy's `gp_…` id as
|
|
372
|
-
policy. The hosted check is keyed/metered — see
|
|
369
|
+
Fail-closed by default (re-throws if the decision can't be obtained). `onError:
|
|
370
|
+
"deny"` fails closed without throwing (clean `allowed: false`). `checkGate()` is
|
|
371
|
+
the lower-level client for the full decision; pass a stored policy's `gp_…` id as
|
|
372
|
+
the policy to reuse a named policy. The hosted check is keyed/metered — see
|
|
373
|
+
`services/dev-api`'s README.
|
|
374
|
+
|
|
375
|
+
> **⚠️ Never use `onError: "allow"` on a real gate.** It FAILS OPEN: on *any*
|
|
376
|
+
> error — a network blip, a timeout, a 5xx, an auth failure, a malformed
|
|
377
|
+
> response — it fabricates a `passed: true` decision and admits the caller
|
|
378
|
+
> **unverified**. Anyone who can make your gate call fail (trivially: induce a
|
|
379
|
+
> timeout) then bypasses the gate completely, so it is worse than no gate at all
|
|
380
|
+
> for anything it protects — a route, a mint, an airdrop, a write, an allowlist.
|
|
381
|
+
> It exists ONLY for soft, non-security personalization where a missing
|
|
382
|
+
> verification must not degrade UX (e.g. optionally showing a "verified"
|
|
383
|
+
> flourish). Using it logs a one-time `console.warn`. If the decision gates
|
|
384
|
+
> anything, use the default `"throw"` or `"deny"`.
|
|
373
385
|
|
|
374
386
|
## What works today
|
|
375
387
|
|
package/dist/gateClient.d.ts
CHANGED
|
@@ -80,14 +80,16 @@ export interface GuardResult {
|
|
|
80
80
|
* silently admitting an unverified caller. `onError: "deny"` instead resolves to
|
|
81
81
|
* a clean `allowed: false` (fail-closed without a throw).
|
|
82
82
|
*
|
|
83
|
-
* ⚠️ SECURITY — `onError: "allow"` FAILS OPEN
|
|
84
|
-
* timeout, 5xx, auth failure, a malformed
|
|
85
|
-
* `passed: true` decision and admits the caller
|
|
86
|
-
*
|
|
87
|
-
* timeout)
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
83
|
+
* ⚠️ SECURITY — `onError: "allow"` FAILS OPEN, and is NEVER acceptable on a real
|
|
84
|
+
* gate. On ANY error (network blip, timeout, 5xx, auth failure, a malformed
|
|
85
|
+
* response) it fabricates a `passed: true` decision and admits the caller
|
|
86
|
+
* UNVERIFIED — so anyone who can make your gate call fail (trivial: induce a
|
|
87
|
+
* timeout) bypasses it completely. That makes it strictly WORSE than no gate for
|
|
88
|
+
* anything it is supposed to protect: a route, a mint, an airdrop, a write, an
|
|
89
|
+
* allowlist. It exists ONLY for soft, non-security personalization where a
|
|
90
|
+
* missing verification must not degrade UX (e.g. optionally showing a "verified"
|
|
91
|
+
* flourish). Constructing a guard with it logs a one-time `console.warn`. If the
|
|
92
|
+
* decision gates ANYTHING, use the default `"throw"` or `"deny"` (fail-closed).
|
|
91
93
|
*
|
|
92
94
|
* @example
|
|
93
95
|
* ```ts
|
package/dist/gateClient.js
CHANGED
|
@@ -81,14 +81,16 @@ export async function checkGate(cfg, subject, policy) {
|
|
|
81
81
|
* silently admitting an unverified caller. `onError: "deny"` instead resolves to
|
|
82
82
|
* a clean `allowed: false` (fail-closed without a throw).
|
|
83
83
|
*
|
|
84
|
-
* ⚠️ SECURITY — `onError: "allow"` FAILS OPEN
|
|
85
|
-
* timeout, 5xx, auth failure, a malformed
|
|
86
|
-
* `passed: true` decision and admits the caller
|
|
87
|
-
*
|
|
88
|
-
* timeout)
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
84
|
+
* ⚠️ SECURITY — `onError: "allow"` FAILS OPEN, and is NEVER acceptable on a real
|
|
85
|
+
* gate. On ANY error (network blip, timeout, 5xx, auth failure, a malformed
|
|
86
|
+
* response) it fabricates a `passed: true` decision and admits the caller
|
|
87
|
+
* UNVERIFIED — so anyone who can make your gate call fail (trivial: induce a
|
|
88
|
+
* timeout) bypasses it completely. That makes it strictly WORSE than no gate for
|
|
89
|
+
* anything it is supposed to protect: a route, a mint, an airdrop, a write, an
|
|
90
|
+
* allowlist. It exists ONLY for soft, non-security personalization where a
|
|
91
|
+
* missing verification must not degrade UX (e.g. optionally showing a "verified"
|
|
92
|
+
* flourish). Constructing a guard with it logs a one-time `console.warn`. If the
|
|
93
|
+
* decision gates ANYTHING, use the default `"throw"` or `"deny"` (fail-closed).
|
|
92
94
|
*
|
|
93
95
|
* @example
|
|
94
96
|
* ```ts
|
|
@@ -106,6 +108,10 @@ export async function checkGate(cfg, subject, policy) {
|
|
|
106
108
|
*/
|
|
107
109
|
export function requireGate(cfg, policy, opts = {}) {
|
|
108
110
|
const onError = opts.onError ?? "throw";
|
|
111
|
+
// Warn HARD (once) the moment a fail-open guard is built — `onError: "allow"`
|
|
112
|
+
// admits unverified callers on any error and must never guard anything real.
|
|
113
|
+
if (onError === "allow")
|
|
114
|
+
warnFailOpenOnce();
|
|
109
115
|
return async (subject) => {
|
|
110
116
|
try {
|
|
111
117
|
const decision = await checkGate(cfg, subject, policy);
|
|
@@ -126,3 +132,16 @@ export function requireGate(cfg, policy, opts = {}) {
|
|
|
126
132
|
}
|
|
127
133
|
};
|
|
128
134
|
}
|
|
135
|
+
/** Process-wide latch so the fail-open warning fires at most once, not on every
|
|
136
|
+
* guard construction. */
|
|
137
|
+
let failOpenWarned = false;
|
|
138
|
+
function warnFailOpenOnce() {
|
|
139
|
+
if (failOpenWarned)
|
|
140
|
+
return;
|
|
141
|
+
failOpenWarned = true;
|
|
142
|
+
// eslint-disable-next-line no-console
|
|
143
|
+
console.warn("[@x1id/resolve] requireGate({ onError: \"allow\" }) FAILS OPEN: on any gate-call " +
|
|
144
|
+
"error it admits the caller UNVERIFIED. Never use it to protect a route, mint, " +
|
|
145
|
+
"airdrop, write, or allowlist — use the default \"throw\" or \"deny\" instead. " +
|
|
146
|
+
"(This warning is for soft personalization-only uses; it logs once.)");
|
|
147
|
+
}
|
package/dist/signin.js
CHANGED
|
@@ -78,6 +78,14 @@ export class SignInError extends Error {
|
|
|
78
78
|
* belt-and-suspenders CR/LF guard in {@link createSignInChallenge} mirrors the
|
|
79
79
|
* `uri`/`statement` fields and is the real anti-injection control. */
|
|
80
80
|
const DOMAIN_RE = /^[A-Za-z0-9.-]{1,253}(:\d{1,5})?$/;
|
|
81
|
+
/** Any character a line-oriented parser could mistake for a boundary: every C0
|
|
82
|
+
* control (so `\n`, `\r`, `\t`, and the rest), DEL, and — the trap the old
|
|
83
|
+
* `\n`/`\r`-only guard missed — the Unicode line/paragraph separators
|
|
84
|
+
* U+2028/U+2029, which `^`/`$` under the regex `m` flag honor just like `\n`
|
|
85
|
+
* (see {@link parseSignInChallenge}'s field parser). FORBIDDEN in every value
|
|
86
|
+
* interpolated into a challenge line (domain, statement, uri) so a crafted
|
|
87
|
+
* value can never forge a second line — challenge-line injection (audit L5). */
|
|
88
|
+
const FORBIDDEN_LINE_CHAR_RE = /[\u0000-\u001f\u007f\u2028\u2029]/;
|
|
81
89
|
/** Nonce charset — ASCII alphanumeric, matching the rest of the SDK's nonces so
|
|
82
90
|
* it can never contain a separator or a newline. SIWE requires >= 8 chars. */
|
|
83
91
|
const NONCE_RE = /^[0-9A-Za-z]{8,128}$/;
|
|
@@ -124,11 +132,14 @@ export function generateSignInNonce(bytes = 16) {
|
|
|
124
132
|
*/
|
|
125
133
|
export function createSignInChallenge(params) {
|
|
126
134
|
const { domain } = params;
|
|
127
|
-
// Explicit
|
|
128
|
-
// makes the anti-challenge-injection intent impossible to
|
|
129
|
-
// charset in DOMAIN_RE is ever loosened.
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
// Explicit control/line-terminator guard first — mirrors the `uri`/`statement`
|
|
136
|
+
// fields below and makes the anti-challenge-injection intent impossible to
|
|
137
|
+
// regress even if the charset in DOMAIN_RE is ever loosened. DOMAIN_RE already
|
|
138
|
+
// admits only `[A-Za-z0-9.-]` (+ optional `:port`), so this is belt-and-
|
|
139
|
+
// suspenders — but it is the control that catches EVERY forbidden boundary
|
|
140
|
+
// char (incl. `\t`, U+2028/U+2029), not just `\n`/`\r` (audit L5).
|
|
141
|
+
if (typeof domain !== "string" || FORBIDDEN_LINE_CHAR_RE.test(domain) || !DOMAIN_RE.test(domain)) {
|
|
142
|
+
throw new SignInError("invalid-params", "domain must be a single DNS host (letters, digits, '.', '-'), no control characters");
|
|
132
143
|
}
|
|
133
144
|
if (!decodeBase58_32(params.address)) {
|
|
134
145
|
throw new SignInError("invalid-params", `"${params.address}" is not a valid X1 address`);
|
|
@@ -137,12 +148,12 @@ export function createSignInChallenge(params) {
|
|
|
137
148
|
// the signer and the verifier both compare against.
|
|
138
149
|
const address = encodeBase58(decodeBase58_32(params.address));
|
|
139
150
|
const statement = params.statement ?? null;
|
|
140
|
-
if (statement !== null && (
|
|
141
|
-
throw new SignInError("invalid-params", "statement must be a single non-empty line");
|
|
151
|
+
if (statement !== null && (FORBIDDEN_LINE_CHAR_RE.test(statement) || statement.length === 0)) {
|
|
152
|
+
throw new SignInError("invalid-params", "statement must be a single non-empty line (no control characters)");
|
|
142
153
|
}
|
|
143
154
|
const uri = params.uri ?? `https://${domain}`;
|
|
144
|
-
if (
|
|
145
|
-
throw new SignInError("invalid-params", "uri must be a single line");
|
|
155
|
+
if (FORBIDDEN_LINE_CHAR_RE.test(uri)) {
|
|
156
|
+
throw new SignInError("invalid-params", "uri must be a single line (no control characters)");
|
|
146
157
|
}
|
|
147
158
|
const nonce = params.nonce ?? generateSignInNonce();
|
|
148
159
|
if (!NONCE_RE.test(nonce)) {
|