jamdesk 1.1.21 → 1.1.23
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/dist/__tests__/integration/validate.integration.test.js +14 -0
- package/dist/__tests__/integration/validate.integration.test.js.map +1 -1
- package/dist/__tests__/unit/validate-password-warning.test.d.ts +2 -0
- package/dist/__tests__/unit/validate-password-warning.test.d.ts.map +1 -0
- package/dist/__tests__/unit/validate-password-warning.test.js +25 -0
- package/dist/__tests__/unit/validate-password-warning.test.js.map +1 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +13 -0
- package/dist/commands/validate.js.map +1 -1
- package/package.json +2 -2
- package/vendored/app/(unlock)/jd/unlock/UnlockForm.tsx +176 -0
- package/vendored/app/(unlock)/jd/unlock/page.tsx +157 -0
- package/vendored/app/api/jd/unlock/route.ts +325 -0
- package/vendored/app/globals.css +14 -14
- package/vendored/app/layout.tsx +29 -4
- package/vendored/lib/auth-resolver.ts +109 -0
- package/vendored/lib/docs-isr.ts +82 -6
- package/vendored/lib/docs-types.ts +26 -0
- package/vendored/lib/extract-highlights.ts +5 -0
- package/vendored/lib/glob-match.ts +83 -0
- package/vendored/lib/middleware-helpers.ts +26 -0
- package/vendored/lib/public-paths-resolver.ts +227 -0
- package/vendored/lib/redis.ts +94 -0
- package/vendored/lib/sanitize-from.ts +36 -0
- package/vendored/postcss.config.mjs +6 -0
- package/vendored/schema/docs-schema.json +50 -0
- package/vendored/shared/auth-cookie.ts +163 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge-safe auth cookie signing and verification.
|
|
3
|
+
*
|
|
4
|
+
* Uses Web Crypto (crypto.subtle) which works in both Vercel Edge Runtime
|
|
5
|
+
* and Node.js v15+. Never import Node 'crypto' here — middleware-helpers.ts
|
|
6
|
+
* is a hard rule about not importing Node modules from edge-bound code.
|
|
7
|
+
*
|
|
8
|
+
* Payload binds slug + host + version + expiresAt. Host binding is
|
|
9
|
+
* essential to prevent cross-host cookie replay between a project
|
|
10
|
+
* subdomain (acme.jamdesk.app) and a custom domain mirror (docs.acme.com)
|
|
11
|
+
* when they share a project slug and version number.
|
|
12
|
+
*
|
|
13
|
+
* Cannot reuse shared/crypto-helpers.ts:secretsEqual because that helper
|
|
14
|
+
* uses Node `crypto.createHmac` for its HMAC-then-timingSafeEqual trick —
|
|
15
|
+
* which is not available in Edge runtime. This module reimplements a
|
|
16
|
+
* minimal constant-time compare over Uint8Array outputs from Web Crypto.
|
|
17
|
+
*
|
|
18
|
+
* Cookie wire format:
|
|
19
|
+
* base64url(`v1.${version}.${expiresAt}.${base64url(HMAC_SHA256(`v1|${slug}|${host}|${version}|${expiresAt}`, secret))}`)
|
|
20
|
+
*
|
|
21
|
+
* The "v1" prefix on both the HMAC input and the body makes it safe to
|
|
22
|
+
* rotate the payload format in the future without silently accepting old
|
|
23
|
+
* cookies with a different binding.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const FORMAT_VERSION = 'v1';
|
|
27
|
+
|
|
28
|
+
export interface AuthCookiePayload {
|
|
29
|
+
slug: string;
|
|
30
|
+
host: string;
|
|
31
|
+
version: number;
|
|
32
|
+
expiresAt: number; // ms since epoch
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface VerifyExpectation {
|
|
36
|
+
slug: string;
|
|
37
|
+
host: string;
|
|
38
|
+
version: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface VerifyResult {
|
|
42
|
+
valid: boolean;
|
|
43
|
+
expiresAt?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function base64url(bytes: Uint8Array): string {
|
|
47
|
+
// Build the binary string with a loop instead of `String.fromCharCode(...bytes)` —
|
|
48
|
+
// the spread form blows the call stack on large buffers (~65k args on V8).
|
|
49
|
+
let bin = '';
|
|
50
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
51
|
+
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function fromBase64url(s: string): Uint8Array {
|
|
55
|
+
// Reject any character outside the base64url alphabet before handing to atob,
|
|
56
|
+
// whose tolerance for bad input varies across runtimes.
|
|
57
|
+
if (!/^[A-Za-z0-9_-]*$/.test(s)) {
|
|
58
|
+
throw new Error('invalid base64url');
|
|
59
|
+
}
|
|
60
|
+
const pad = s.length % 4 === 0 ? '' : '='.repeat(4 - (s.length % 4));
|
|
61
|
+
const bin = atob(s.replace(/-/g, '+').replace(/_/g, '/') + pad);
|
|
62
|
+
const bytes = new Uint8Array(bin.length);
|
|
63
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
64
|
+
return bytes;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function importKey(secret: string): Promise<CryptoKey> {
|
|
68
|
+
return crypto.subtle.importKey(
|
|
69
|
+
'raw',
|
|
70
|
+
new TextEncoder().encode(secret),
|
|
71
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
72
|
+
false,
|
|
73
|
+
['sign', 'verify']
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function hmac(secret: string, data: string): Promise<Uint8Array> {
|
|
78
|
+
const key = await importKey(secret);
|
|
79
|
+
const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(data));
|
|
80
|
+
return new Uint8Array(sig);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Constant-time compare over Uint8Arrays. Cannot reuse shared/crypto-helpers.ts:secretsEqual
|
|
85
|
+
* because that helper uses Node `crypto.createHmac` which is not available in Edge runtime.
|
|
86
|
+
*/
|
|
87
|
+
function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
88
|
+
if (a.length !== b.length) return false;
|
|
89
|
+
let diff = 0;
|
|
90
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
91
|
+
return diff === 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildSignInput(
|
|
95
|
+
slug: string,
|
|
96
|
+
host: string,
|
|
97
|
+
version: number,
|
|
98
|
+
expiresAt: number
|
|
99
|
+
): string {
|
|
100
|
+
// Use a delimiter that cannot appear in slug or host (both are bounded to
|
|
101
|
+
// [a-z0-9.-]) — '|' guarantees no field boundary ambiguity.
|
|
102
|
+
return `${FORMAT_VERSION}|${slug}|${host}|${version}|${expiresAt}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Sign a cookie payload. Returns the full cookie value string to put in Set-Cookie.
|
|
107
|
+
*/
|
|
108
|
+
export async function signAuthCookie(
|
|
109
|
+
payload: AuthCookiePayload,
|
|
110
|
+
secret: string
|
|
111
|
+
): Promise<string> {
|
|
112
|
+
const sig = await hmac(
|
|
113
|
+
secret,
|
|
114
|
+
buildSignInput(payload.slug, payload.host, payload.version, payload.expiresAt)
|
|
115
|
+
);
|
|
116
|
+
// Body carries only the fields the client needs to decode; slug and host
|
|
117
|
+
// are known at verify time from the request itself, so they're not transmitted.
|
|
118
|
+
const body = `${FORMAT_VERSION}.${payload.version}.${payload.expiresAt}.${base64url(sig)}`;
|
|
119
|
+
return base64url(new TextEncoder().encode(body));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Verify a cookie value against the expected slug, host, and version.
|
|
124
|
+
* Returns { valid: false } on any failure (including malformed input).
|
|
125
|
+
*/
|
|
126
|
+
export async function verifyAuthCookie(
|
|
127
|
+
cookie: string,
|
|
128
|
+
expected: VerifyExpectation,
|
|
129
|
+
secret: string
|
|
130
|
+
): Promise<VerifyResult> {
|
|
131
|
+
try {
|
|
132
|
+
if (!cookie) return { valid: false };
|
|
133
|
+
const bodyBytes = fromBase64url(cookie);
|
|
134
|
+
const body = new TextDecoder().decode(bodyBytes);
|
|
135
|
+
const parts = body.split('.');
|
|
136
|
+
if (parts.length !== 4) return { valid: false };
|
|
137
|
+
|
|
138
|
+
const [formatVersion, versionStr, expiresAtStr, sigB64] = parts;
|
|
139
|
+
if (formatVersion !== FORMAT_VERSION) return { valid: false };
|
|
140
|
+
|
|
141
|
+
const version = Number(versionStr);
|
|
142
|
+
const expiresAt = Number(expiresAtStr);
|
|
143
|
+
// Use isSafeInteger to reject floats, Infinity, NaN, and scientific notation
|
|
144
|
+
// like "1e100" — our version and expiresAt fields are always integers.
|
|
145
|
+
if (!Number.isSafeInteger(version) || !Number.isSafeInteger(expiresAt)) {
|
|
146
|
+
return { valid: false };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (version !== expected.version) return { valid: false };
|
|
150
|
+
if (expiresAt <= Date.now()) return { valid: false };
|
|
151
|
+
|
|
152
|
+
const expectedSig = await hmac(
|
|
153
|
+
secret,
|
|
154
|
+
buildSignInput(expected.slug, expected.host, version, expiresAt)
|
|
155
|
+
);
|
|
156
|
+
const actualSig = fromBase64url(sigB64);
|
|
157
|
+
if (!constantTimeEqual(expectedSig, actualSig)) return { valid: false };
|
|
158
|
+
|
|
159
|
+
return { valid: true, expiresAt };
|
|
160
|
+
} catch {
|
|
161
|
+
return { valid: false };
|
|
162
|
+
}
|
|
163
|
+
}
|