@invokable/server 0.1.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/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/checkpoint-routes.d.ts +40 -0
- package/dist/checkpoint-routes.d.ts.map +1 -0
- package/dist/checkpoint-routes.js +112 -0
- package/dist/checkpoint-routes.js.map +1 -0
- package/dist/checkpoints.d.ts +93 -0
- package/dist/checkpoints.d.ts.map +1 -0
- package/dist/checkpoints.js +179 -0
- package/dist/checkpoints.js.map +1 -0
- package/dist/handler.d.ts +48 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +221 -0
- package/dist/handler.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/node.d.ts +10 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +90 -0
- package/dist/node.js.map +1 -0
- package/dist/stable-json.d.ts +11 -0
- package/dist/stable-json.d.ts.map +1 -0
- package/dist/stable-json.js +41 -0
- package/dist/stable-json.js.map +1 -0
- package/dist/store.d.ts +48 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +38 -0
- package/dist/store.js.map +1 -0
- package/dist/tokens.d.ts +11 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +47 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +41 -0
package/dist/handler.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { generateDeviceCode, generateToken, generateUserCode, hashToken, } from './tokens.js';
|
|
2
|
+
const DEFAULT_DEVICE_TTL_MS = 15 * 60 * 1000;
|
|
3
|
+
const DEFAULT_POLL_INTERVAL_S = 5;
|
|
4
|
+
function json(body, status = 200) {
|
|
5
|
+
return new Response(JSON.stringify(body), {
|
|
6
|
+
status,
|
|
7
|
+
headers: { 'content-type': 'application/json' },
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
function html(body, status = 200) {
|
|
11
|
+
return new Response(body, { status, headers: { 'content-type': 'text/html; charset=utf-8' } });
|
|
12
|
+
}
|
|
13
|
+
function escapeHtml(s) {
|
|
14
|
+
return s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
15
|
+
}
|
|
16
|
+
function bearerToken(request) {
|
|
17
|
+
const header = request.headers.get('authorization');
|
|
18
|
+
if (!header?.startsWith('Bearer '))
|
|
19
|
+
return null;
|
|
20
|
+
const value = header.slice(7).trim();
|
|
21
|
+
return value || null;
|
|
22
|
+
}
|
|
23
|
+
function defaultApprovePage(ctx) {
|
|
24
|
+
const { device, user, userCode } = ctx;
|
|
25
|
+
if (!user) {
|
|
26
|
+
return `<!doctype html><meta charset="utf-8"><title>Sign in required</title>
|
|
27
|
+
<h1>Sign in required</h1>
|
|
28
|
+
<p>Sign in, then return to this page to approve the code <code>${escapeHtml(userCode)}</code>.</p>`;
|
|
29
|
+
}
|
|
30
|
+
if (!device) {
|
|
31
|
+
return `<!doctype html><meta charset="utf-8"><title>Unknown code</title>
|
|
32
|
+
<h1>That code is not recognised</h1>
|
|
33
|
+
<p>It may have expired. Run the login command again to get a new one.</p>`;
|
|
34
|
+
}
|
|
35
|
+
// The device's self-reported details are shown so the user can tell a
|
|
36
|
+
// legitimate request from a code someone else asked them to approve.
|
|
37
|
+
return `<!doctype html><meta charset="utf-8"><title>Approve device</title>
|
|
38
|
+
<h1>Approve this device?</h1>
|
|
39
|
+
<dl>
|
|
40
|
+
<dt>Tool</dt><dd>${escapeHtml(device.clientName)} ${escapeHtml(device.toolVersion)}</dd>
|
|
41
|
+
<dt>Machine</dt><dd>${escapeHtml(device.hostname)}</dd>
|
|
42
|
+
<dt>Code</dt><dd><code>${escapeHtml(device.userCode)}</code></dd>
|
|
43
|
+
<dt>Signed in as</dt><dd>${escapeHtml(user.displayName ?? user.subject)}</dd>
|
|
44
|
+
</dl>
|
|
45
|
+
<p><strong>Only approve this if you just started a login on that machine.</strong></p>
|
|
46
|
+
<form method="post" action="./device/approve">
|
|
47
|
+
<input type="hidden" name="userCode" value="${escapeHtml(device.userCode)}">
|
|
48
|
+
<button type="submit" name="decision" value="approve">Approve</button>
|
|
49
|
+
<button type="submit" name="decision" value="deny">Deny</button>
|
|
50
|
+
</form>`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The five device-flow endpoints of spec 5.4, as a fetch-style handler:
|
|
54
|
+
*
|
|
55
|
+
* POST /device/start → device + user code
|
|
56
|
+
* GET /device?code=… → approval page
|
|
57
|
+
* POST /device/approve → records the user's decision (session required)
|
|
58
|
+
* POST /device/token → polled by the CLI until approved
|
|
59
|
+
* POST /cli/logout → revokes the bearer token
|
|
60
|
+
* GET /cli/whoami → identity behind the bearer token
|
|
61
|
+
*
|
|
62
|
+
* Returns null for paths it does not own, so a host application can mount it
|
|
63
|
+
* alongside its own routes.
|
|
64
|
+
*/
|
|
65
|
+
export function invokableAuth(options) {
|
|
66
|
+
const { store, requireSession, approvePage = defaultApprovePage, tokenPrefix = 'ivk', tokenTtl = null, deviceCodeTtlMs = DEFAULT_DEVICE_TTL_MS, pollIntervalSeconds = DEFAULT_POLL_INTERVAL_S, now = () => Date.now(), } = options;
|
|
67
|
+
async function authenticate(request) {
|
|
68
|
+
const token = bearerToken(request);
|
|
69
|
+
if (!token)
|
|
70
|
+
return null;
|
|
71
|
+
const record = await store.findTokenByHash(hashToken(token));
|
|
72
|
+
if (!record)
|
|
73
|
+
return null;
|
|
74
|
+
if (record.revokedAt)
|
|
75
|
+
return null;
|
|
76
|
+
if (record.expiresAt !== null && record.expiresAt <= now())
|
|
77
|
+
return null;
|
|
78
|
+
return record;
|
|
79
|
+
}
|
|
80
|
+
return async function handle(request) {
|
|
81
|
+
const url = new URL(request.url);
|
|
82
|
+
const path = url.pathname.replace(/\/+$/, '') || '/';
|
|
83
|
+
const method = request.method.toUpperCase();
|
|
84
|
+
// ---- POST /device/start -------------------------------------------------
|
|
85
|
+
if (path.endsWith('/device/start') && method === 'POST') {
|
|
86
|
+
const body = (await request.json().catch(() => ({})));
|
|
87
|
+
const at = now();
|
|
88
|
+
const record = {
|
|
89
|
+
deviceCode: generateDeviceCode(),
|
|
90
|
+
userCode: generateUserCode(),
|
|
91
|
+
state: 'pending',
|
|
92
|
+
clientName: String(body['clientName'] ?? 'unknown'),
|
|
93
|
+
hostname: String(body['hostname'] ?? 'unknown'),
|
|
94
|
+
toolVersion: String(body['toolVersion'] ?? 'unknown'),
|
|
95
|
+
createdAt: at,
|
|
96
|
+
expiresAt: at + deviceCodeTtlMs,
|
|
97
|
+
};
|
|
98
|
+
await store.createDevice(record);
|
|
99
|
+
const verificationUri = `${url.origin}${url.pathname.replace(/\/start$/, '')}`;
|
|
100
|
+
return json({
|
|
101
|
+
deviceCode: record.deviceCode,
|
|
102
|
+
userCode: record.userCode,
|
|
103
|
+
verificationUri,
|
|
104
|
+
verificationUriComplete: `${verificationUri}?code=${record.userCode}`,
|
|
105
|
+
interval: pollIntervalSeconds,
|
|
106
|
+
expiresIn: Math.floor(deviceCodeTtlMs / 1000),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// ---- POST /device/token -------------------------------------------------
|
|
110
|
+
if (path.endsWith('/device/token') && method === 'POST') {
|
|
111
|
+
const body = (await request.json().catch(() => ({})));
|
|
112
|
+
const deviceCode = String(body['deviceCode'] ?? '');
|
|
113
|
+
const device = await store.findDeviceByDeviceCode(deviceCode);
|
|
114
|
+
const at = now();
|
|
115
|
+
if (!device)
|
|
116
|
+
return json({ error: 'expired_token' }, 400);
|
|
117
|
+
if (device.expiresAt <= at)
|
|
118
|
+
return json({ error: 'expired_token' }, 400);
|
|
119
|
+
// Polling faster than the advertised interval earns a back-off rather
|
|
120
|
+
// than a token; this is the only rate limit the flow needs.
|
|
121
|
+
if (device.lastPolledAt && at - device.lastPolledAt < pollIntervalSeconds * 1000) {
|
|
122
|
+
await store.updateDevice(deviceCode, { lastPolledAt: at });
|
|
123
|
+
return json({ error: 'slow_down', interval: pollIntervalSeconds * 2 }, 400);
|
|
124
|
+
}
|
|
125
|
+
await store.updateDevice(deviceCode, { lastPolledAt: at });
|
|
126
|
+
if (device.state === 'denied')
|
|
127
|
+
return json({ error: 'access_denied' }, 400);
|
|
128
|
+
if (device.state === 'consumed')
|
|
129
|
+
return json({ error: 'expired_token' }, 400);
|
|
130
|
+
if (device.state === 'pending')
|
|
131
|
+
return json({ error: 'authorization_pending' }, 400);
|
|
132
|
+
// Approved: issue once, then burn the device code so a leaked code cannot
|
|
133
|
+
// be replayed into a second credential.
|
|
134
|
+
const { token, tokenHash } = generateToken(tokenPrefix);
|
|
135
|
+
const record = {
|
|
136
|
+
tokenHash,
|
|
137
|
+
tokenPrefix,
|
|
138
|
+
subject: device.subject ?? 'unknown',
|
|
139
|
+
...(device.orgId !== undefined ? { orgId: device.orgId } : {}),
|
|
140
|
+
clientName: device.clientName,
|
|
141
|
+
hostname: device.hostname,
|
|
142
|
+
createdAt: at,
|
|
143
|
+
expiresAt: tokenTtl === null ? null : at + tokenTtl,
|
|
144
|
+
};
|
|
145
|
+
await store.createToken(record);
|
|
146
|
+
await store.updateDevice(deviceCode, { state: 'consumed' });
|
|
147
|
+
return json({
|
|
148
|
+
token,
|
|
149
|
+
tokenPrefix,
|
|
150
|
+
orgId: record.orgId ?? null,
|
|
151
|
+
subject: record.subject,
|
|
152
|
+
webOrigin: url.origin,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
// ---- POST /device/approve ----------------------------------------------
|
|
156
|
+
if (path.endsWith('/device/approve') && method === 'POST') {
|
|
157
|
+
const user = await requireSession(request);
|
|
158
|
+
if (!user)
|
|
159
|
+
return json({ error: 'unauthorized', message: 'Sign in first.' }, 401);
|
|
160
|
+
const contentType = request.headers.get('content-type') ?? '';
|
|
161
|
+
let userCode = '';
|
|
162
|
+
let decision = 'approve';
|
|
163
|
+
if (contentType.includes('application/json')) {
|
|
164
|
+
const body = (await request.json().catch(() => ({})));
|
|
165
|
+
userCode = String(body['userCode'] ?? '');
|
|
166
|
+
decision = String(body['decision'] ?? 'approve');
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const form = await request.formData().catch(() => null);
|
|
170
|
+
userCode = String(form?.get('userCode') ?? '');
|
|
171
|
+
decision = String(form?.get('decision') ?? 'approve');
|
|
172
|
+
}
|
|
173
|
+
const device = await store.findDeviceByUserCode(userCode.trim().toUpperCase());
|
|
174
|
+
if (!device)
|
|
175
|
+
return json({ error: 'not_found', message: 'Unknown code.' }, 404);
|
|
176
|
+
if (device.expiresAt <= now())
|
|
177
|
+
return json({ error: 'expired', message: 'Code expired.' }, 410);
|
|
178
|
+
if (device.state !== 'pending') {
|
|
179
|
+
return json({ error: 'conflict', message: 'That code was already used.' }, 409);
|
|
180
|
+
}
|
|
181
|
+
await store.updateDevice(device.deviceCode, {
|
|
182
|
+
state: decision === 'deny' ? 'denied' : 'approved',
|
|
183
|
+
subject: user.subject,
|
|
184
|
+
...(user.orgId !== undefined ? { orgId: user.orgId } : {}),
|
|
185
|
+
});
|
|
186
|
+
return json({ ok: true, decision: decision === 'deny' ? 'denied' : 'approved' });
|
|
187
|
+
}
|
|
188
|
+
// ---- GET /device --------------------------------------------------------
|
|
189
|
+
if (path.endsWith('/device') && method === 'GET') {
|
|
190
|
+
const userCode = (url.searchParams.get('code') ?? '').trim().toUpperCase();
|
|
191
|
+
const user = await requireSession(request);
|
|
192
|
+
const device = userCode ? await store.findDeviceByUserCode(userCode) : null;
|
|
193
|
+
return html(await approvePage({ userCode, device, user }));
|
|
194
|
+
}
|
|
195
|
+
// ---- GET /cli/whoami ----------------------------------------------------
|
|
196
|
+
if (path.endsWith('/cli/whoami') && method === 'GET') {
|
|
197
|
+
const record = await authenticate(request);
|
|
198
|
+
if (!record) {
|
|
199
|
+
return json({ error: 'unauthorized', message: 'Token rejected.' }, 401);
|
|
200
|
+
}
|
|
201
|
+
return json({
|
|
202
|
+
subject: record.subject,
|
|
203
|
+
orgId: record.orgId ?? null,
|
|
204
|
+
tokenPrefix: record.tokenPrefix,
|
|
205
|
+
clientName: record.clientName,
|
|
206
|
+
hostname: record.hostname,
|
|
207
|
+
createdAt: new Date(record.createdAt).toISOString(),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
// ---- POST /cli/logout ---------------------------------------------------
|
|
211
|
+
if (path.endsWith('/cli/logout') && method === 'POST') {
|
|
212
|
+
const record = await authenticate(request);
|
|
213
|
+
if (!record)
|
|
214
|
+
return json({ error: 'unauthorized', message: 'Token rejected.' }, 401);
|
|
215
|
+
await store.revokeToken(record.tokenHash, now());
|
|
216
|
+
return json({ revoked: true });
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.js","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { invokableAuth } from './handler.js';
|
|
2
|
+
export type { InvokableAuthOptions, InvokableAuthHandler, SessionUser, ApprovePageContext, } from './handler.js';
|
|
3
|
+
export { memoryStore } from './store.js';
|
|
4
|
+
export type { AuthStore, DeviceRecord, TokenRecord, DeviceState } from './store.js';
|
|
5
|
+
export { generateToken, hashToken, generateUserCode, generateDeviceCode, safeEqual, } from './tokens.js';
|
|
6
|
+
export { checkpointRoutes, verifyCheckpoint, staleResponse } from './checkpoint-routes.js';
|
|
7
|
+
export type { CheckpointRoutesOptions, VerifyCheckpointOptions } from './checkpoint-routes.js';
|
|
8
|
+
export { CheckpointVerifier, computeFingerprint, hashSummary, memoryCheckpointStore, parseCheckpointHeader, } from './checkpoints.js';
|
|
9
|
+
export type { CheckpointRecord, CheckpointStore, CheckpointVerifierOptions, CheckpointFailure, VerifyResult, } from './checkpoints.js';
|
|
10
|
+
export { stableStringify } from './stable-json.js';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EACV,oBAAoB,EACpB,oBAAoB,EACpB,WAAW,EACX,kBAAkB,GACnB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEpF,OAAO,EACL,aAAa,EACb,SAAS,EACT,gBAAgB,EAChB,kBAAkB,EAClB,SAAS,GACV,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC3F,YAAY,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAE/F,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,WAAW,EACX,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,yBAAyB,EACzB,iBAAiB,EACjB,YAAY,GACb,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { invokableAuth } from './handler.js';
|
|
2
|
+
export { memoryStore } from './store.js';
|
|
3
|
+
export { generateToken, hashToken, generateUserCode, generateDeviceCode, safeEqual, } from './tokens.js';
|
|
4
|
+
export { checkpointRoutes, verifyCheckpoint, staleResponse } from './checkpoint-routes.js';
|
|
5
|
+
export { CheckpointVerifier, computeFingerprint, hashSummary, memoryCheckpointStore, parseCheckpointHeader, } from './checkpoints.js';
|
|
6
|
+
export { stableStringify } from './stable-json.js';
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAQ7C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAGzC,OAAO,EACL,aAAa,EACb,SAAS,EACT,gBAAgB,EAChB,kBAAkB,EAClB,SAAS,GACV,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAG3F,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,WAAW,EACX,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAS1B,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { InvokableAuthHandler } from './handler.js';
|
|
3
|
+
/**
|
|
4
|
+
* Express-style middleware. Calls `next()` for paths the handler does not own,
|
|
5
|
+
* so it composes with the host application's routes.
|
|
6
|
+
*/
|
|
7
|
+
export declare function expressMiddleware(handler: InvokableAuthHandler): (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => void;
|
|
8
|
+
/** Plain `node:http` listener. Answers 404 for unowned paths. */
|
|
9
|
+
export declare function nodeListener(handler: InvokableAuthHandler): (req: IncomingMessage, res: ServerResponse) => void;
|
|
10
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAkDzD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,oBAAoB,IAE3D,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,MAAM,CAAC,GAAG,CAAC,EAAE,OAAO,KAAK,IAAI,KAC5B,IAAI,CAYR;AAED,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,OAAO,EAAE,oBAAoB,IAC/B,KAAK,eAAe,EAAE,KAAK,cAAc,KAAG,IAAI,CAmB1E"}
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapts the fetch-style handler to Node's `http` and to Express, whose
|
|
3
|
+
* req/res are Node's. Hono, Deno and workers can use the handler directly.
|
|
4
|
+
*/
|
|
5
|
+
function toRequest(req, body) {
|
|
6
|
+
const host = req.headers.host ?? 'localhost';
|
|
7
|
+
const proto = req.headers['x-forwarded-proto'] ?? 'http';
|
|
8
|
+
const url = new URL(req.url ?? '/', `${proto}://${host}`);
|
|
9
|
+
const headers = new Headers();
|
|
10
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
11
|
+
if (value === undefined)
|
|
12
|
+
continue;
|
|
13
|
+
if (Array.isArray(value))
|
|
14
|
+
value.forEach((v) => headers.append(key, v));
|
|
15
|
+
else
|
|
16
|
+
headers.set(key, value);
|
|
17
|
+
}
|
|
18
|
+
const method = req.method ?? 'GET';
|
|
19
|
+
const hasBody = method !== 'GET' && method !== 'HEAD' && body.length > 0;
|
|
20
|
+
return new Request(url, {
|
|
21
|
+
method,
|
|
22
|
+
headers,
|
|
23
|
+
...(hasBody ? { body: new Uint8Array(body) } : {}),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async function readBody(req) {
|
|
27
|
+
// Express's json/urlencoded middleware may have consumed the stream already.
|
|
28
|
+
const parsed = req.body;
|
|
29
|
+
if (parsed !== undefined && parsed !== null) {
|
|
30
|
+
if (Buffer.isBuffer(parsed))
|
|
31
|
+
return parsed;
|
|
32
|
+
if (typeof parsed === 'string')
|
|
33
|
+
return Buffer.from(parsed);
|
|
34
|
+
return Buffer.from(JSON.stringify(parsed));
|
|
35
|
+
}
|
|
36
|
+
const chunks = [];
|
|
37
|
+
for await (const chunk of req)
|
|
38
|
+
chunks.push(chunk);
|
|
39
|
+
return Buffer.concat(chunks);
|
|
40
|
+
}
|
|
41
|
+
async function send(res, response) {
|
|
42
|
+
const headers = {};
|
|
43
|
+
response.headers.forEach((value, key) => {
|
|
44
|
+
headers[key] = value;
|
|
45
|
+
});
|
|
46
|
+
res.writeHead(response.status, headers);
|
|
47
|
+
res.end(Buffer.from(await response.arrayBuffer()));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Express-style middleware. Calls `next()` for paths the handler does not own,
|
|
51
|
+
* so it composes with the host application's routes.
|
|
52
|
+
*/
|
|
53
|
+
export function expressMiddleware(handler) {
|
|
54
|
+
return function middleware(req, res, next) {
|
|
55
|
+
void (async () => {
|
|
56
|
+
try {
|
|
57
|
+
const body = await readBody(req);
|
|
58
|
+
const response = await handler(toRequest(req, body));
|
|
59
|
+
if (response === null)
|
|
60
|
+
return next();
|
|
61
|
+
await send(res, response);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
next(e);
|
|
65
|
+
}
|
|
66
|
+
})();
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Plain `node:http` listener. Answers 404 for unowned paths. */
|
|
70
|
+
export function nodeListener(handler) {
|
|
71
|
+
return function listener(req, res) {
|
|
72
|
+
void (async () => {
|
|
73
|
+
try {
|
|
74
|
+
const body = await readBody(req);
|
|
75
|
+
const response = await handler(toRequest(req, body));
|
|
76
|
+
if (response === null) {
|
|
77
|
+
res.writeHead(404, { 'content-type': 'application/json' });
|
|
78
|
+
res.end(JSON.stringify({ error: 'not_found' }));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await send(res, response);
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
res.writeHead(500, { 'content-type': 'application/json' });
|
|
85
|
+
res.end(JSON.stringify({ error: 'internal', message: e instanceof Error ? e.message : String(e) }));
|
|
86
|
+
}
|
|
87
|
+
})();
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=node.js.map
|
package/dist/node.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.js","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic JSON, byte-identical to `@invokable/core`'s implementation.
|
|
3
|
+
*
|
|
4
|
+
* Duplicated rather than imported: the server must not depend on the client
|
|
5
|
+
* package, and the two are pinned together by the shared test in
|
|
6
|
+
* `test/canonical-parity.test.ts`. If they ever diverge, every approval issued
|
|
7
|
+
* by one and verified by the other looks stale — so the parity test is the
|
|
8
|
+
* thing that keeps this honest.
|
|
9
|
+
*/
|
|
10
|
+
export declare function stableStringify(value: unknown): string;
|
|
11
|
+
//# sourceMappingURL=stable-json.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stable-json.d.ts","sourceRoot":"","sources":["../src/stable-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEtD"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic JSON, byte-identical to `@invokable/core`'s implementation.
|
|
3
|
+
*
|
|
4
|
+
* Duplicated rather than imported: the server must not depend on the client
|
|
5
|
+
* package, and the two are pinned together by the shared test in
|
|
6
|
+
* `test/canonical-parity.test.ts`. If they ever diverge, every approval issued
|
|
7
|
+
* by one and verified by the other looks stale — so the parity test is the
|
|
8
|
+
* thing that keeps this honest.
|
|
9
|
+
*/
|
|
10
|
+
export function stableStringify(value) {
|
|
11
|
+
return serialise(value);
|
|
12
|
+
}
|
|
13
|
+
function serialise(value) {
|
|
14
|
+
if (value === null)
|
|
15
|
+
return 'null';
|
|
16
|
+
const type = typeof value;
|
|
17
|
+
if (type === 'number') {
|
|
18
|
+
if (!Number.isFinite(value)) {
|
|
19
|
+
throw new TypeError(`Cannot canonicalise non-finite number: ${String(value)}`);
|
|
20
|
+
}
|
|
21
|
+
return JSON.stringify(value);
|
|
22
|
+
}
|
|
23
|
+
if (type === 'string' || type === 'boolean')
|
|
24
|
+
return JSON.stringify(value);
|
|
25
|
+
if (type === 'undefined' || type === 'function' || type === 'symbol') {
|
|
26
|
+
throw new TypeError(`Cannot canonicalise value of type ${type}`);
|
|
27
|
+
}
|
|
28
|
+
if (type === 'bigint')
|
|
29
|
+
return JSON.stringify(value.toString());
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
return '[' + value.map((v) => serialise(v === undefined ? null : v)).join(',') + ']';
|
|
32
|
+
}
|
|
33
|
+
if (value instanceof Date)
|
|
34
|
+
return JSON.stringify(value.toISOString());
|
|
35
|
+
const obj = value;
|
|
36
|
+
const keys = Object.keys(obj)
|
|
37
|
+
.filter((k) => obj[k] !== undefined)
|
|
38
|
+
.sort();
|
|
39
|
+
return '{' + keys.map((k) => `${JSON.stringify(k)}:${serialise(obj[k])}`).join(',') + '}';
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=stable-json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stable-json.js","sourceRoot":"","sources":["../src/stable-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,SAAS,CAAC,KAAc;IAC/B,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAElC,MAAM,IAAI,GAAG,OAAO,KAAK,CAAC;IAC1B,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAe,CAAC,EAAE,CAAC;YACtC,MAAM,IAAI,SAAS,CAAC,0CAA0C,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACjF,CAAC;QACD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IACD,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC1E,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,UAAU,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACrE,MAAM,IAAI,SAAS,CAAC,qCAAqC,IAAI,EAAE,CAAC,CAAC;IACnE,CAAC;IACD,IAAI,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,SAAS,CAAE,KAAgB,CAAC,QAAQ,EAAE,CAAC,CAAC;IAE3E,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACvF,CAAC;IAED,IAAI,KAAK,YAAY,IAAI;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;IAEtE,MAAM,GAAG,GAAG,KAAgC,CAAC;IAC7C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;SAC1B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC;SACnC,IAAI,EAAE,CAAC;IACV,OAAO,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;AAC5F,CAAC"}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type DeviceState = 'pending' | 'approved' | 'denied' | 'consumed';
|
|
2
|
+
export interface DeviceRecord {
|
|
3
|
+
deviceCode: string;
|
|
4
|
+
userCode: string;
|
|
5
|
+
state: DeviceState;
|
|
6
|
+
/** What the CLI reported about itself at /device/start. */
|
|
7
|
+
clientName: string;
|
|
8
|
+
hostname: string;
|
|
9
|
+
toolVersion: string;
|
|
10
|
+
createdAt: number;
|
|
11
|
+
expiresAt: number;
|
|
12
|
+
/** Set when a signed-in user approves. */
|
|
13
|
+
subject?: string;
|
|
14
|
+
orgId?: string;
|
|
15
|
+
/** Enforces the polling interval. */
|
|
16
|
+
lastPolledAt?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface TokenRecord {
|
|
19
|
+
tokenHash: string;
|
|
20
|
+
tokenPrefix: string;
|
|
21
|
+
subject: string;
|
|
22
|
+
orgId?: string;
|
|
23
|
+
clientName: string;
|
|
24
|
+
hostname: string;
|
|
25
|
+
createdAt: number;
|
|
26
|
+
/** Null means long-lived: revocation only (spec 5.5 `tokenTtl: null`). */
|
|
27
|
+
expiresAt: number | null;
|
|
28
|
+
revokedAt?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Persistence boundary. `memoryStore()` is for development and tests; a Postgres
|
|
32
|
+
* implementation is P1 in the spec. Everything is async so a real database drops
|
|
33
|
+
* in without changing the handlers.
|
|
34
|
+
*/
|
|
35
|
+
export interface AuthStore {
|
|
36
|
+
createDevice(record: DeviceRecord): Promise<void>;
|
|
37
|
+
findDeviceByDeviceCode(deviceCode: string): Promise<DeviceRecord | null>;
|
|
38
|
+
findDeviceByUserCode(userCode: string): Promise<DeviceRecord | null>;
|
|
39
|
+
updateDevice(deviceCode: string, patch: Partial<DeviceRecord>): Promise<void>;
|
|
40
|
+
createToken(record: TokenRecord): Promise<void>;
|
|
41
|
+
findTokenByHash(tokenHash: string): Promise<TokenRecord | null>;
|
|
42
|
+
revokeToken(tokenHash: string, at: number): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
export declare function memoryStore(): AuthStore & {
|
|
45
|
+
_devices: Map<string, DeviceRecord>;
|
|
46
|
+
_tokens: Map<string, TokenRecord>;
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEzE,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;IACnB,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IACzE,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IACrE,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9E,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAChE,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3D;AAED,wBAAgB,WAAW,IAAI,SAAS,GAAG;IAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;CAAE,CAoCpH"}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function memoryStore() {
|
|
2
|
+
const devices = new Map();
|
|
3
|
+
const tokens = new Map();
|
|
4
|
+
return {
|
|
5
|
+
_devices: devices,
|
|
6
|
+
_tokens: tokens,
|
|
7
|
+
async createDevice(record) {
|
|
8
|
+
devices.set(record.deviceCode, record);
|
|
9
|
+
},
|
|
10
|
+
async findDeviceByDeviceCode(deviceCode) {
|
|
11
|
+
return devices.get(deviceCode) ?? null;
|
|
12
|
+
},
|
|
13
|
+
async findDeviceByUserCode(userCode) {
|
|
14
|
+
for (const d of devices.values()) {
|
|
15
|
+
if (d.userCode === userCode)
|
|
16
|
+
return d;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
},
|
|
20
|
+
async updateDevice(deviceCode, patch) {
|
|
21
|
+
const existing = devices.get(deviceCode);
|
|
22
|
+
if (existing)
|
|
23
|
+
devices.set(deviceCode, { ...existing, ...patch });
|
|
24
|
+
},
|
|
25
|
+
async createToken(record) {
|
|
26
|
+
tokens.set(record.tokenHash, record);
|
|
27
|
+
},
|
|
28
|
+
async findTokenByHash(tokenHash) {
|
|
29
|
+
return tokens.get(tokenHash) ?? null;
|
|
30
|
+
},
|
|
31
|
+
async revokeToken(tokenHash, at) {
|
|
32
|
+
const existing = tokens.get(tokenHash);
|
|
33
|
+
if (existing)
|
|
34
|
+
tokens.set(tokenHash, { ...existing, revokedAt: at });
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAgDA,MAAM,UAAU,WAAW;IACzB,MAAM,OAAO,GAAG,IAAI,GAAG,EAAwB,CAAC;IAChD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAuB,CAAC;IAE9C,OAAO;QACL,QAAQ,EAAE,OAAO;QACjB,OAAO,EAAE,MAAM;QAEf,KAAK,CAAC,YAAY,CAAC,MAAM;YACvB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACzC,CAAC;QACD,KAAK,CAAC,sBAAsB,CAAC,UAAU;YACrC,OAAO,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC;QACzC,CAAC;QACD,KAAK,CAAC,oBAAoB,CAAC,QAAQ;YACjC,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;gBACjC,IAAI,CAAC,CAAC,QAAQ,KAAK,QAAQ;oBAAE,OAAO,CAAC,CAAC;YACxC,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,KAAK,CAAC,YAAY,CAAC,UAAU,EAAE,KAAK;YAClC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YACzC,IAAI,QAAQ;gBAAE,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,EAAE,GAAG,QAAQ,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;QACnE,CAAC;QAED,KAAK,CAAC,WAAW,CAAC,MAAM;YACtB,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QACvC,CAAC;QACD,KAAK,CAAC,eAAe,CAAC,SAAS;YAC7B,OAAO,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC;QACvC,CAAC;QACD,KAAK,CAAC,WAAW,CAAC,SAAS,EAAE,EAAE;YAC7B,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACvC,IAAI,QAAQ;gBAAE,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,GAAG,QAAQ,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,CAAC;QACtE,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/tokens.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function generateToken(prefix: string): {
|
|
2
|
+
token: string;
|
|
3
|
+
tokenHash: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function hashToken(token: string): string;
|
|
6
|
+
/** `WXYZ-1234`, from an alphabet chosen to survive being read over a phone. */
|
|
7
|
+
export declare function generateUserCode(): string;
|
|
8
|
+
export declare function generateDeviceCode(): string;
|
|
9
|
+
/** Constant-time comparison for values an attacker can supply repeatedly. */
|
|
10
|
+
export declare function safeEqual(a: string, b: string): boolean;
|
|
11
|
+
//# sourceMappingURL=tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAsBA,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAGlF;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,IAAI,MAAM,CAQzC;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,6EAA6E;AAC7E,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAKvD"}
|
package/dist/tokens.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Token format (spec 5.4): `<prefix>_<32 bytes base62>`.
|
|
4
|
+
*
|
|
5
|
+
* The plaintext is returned to the client exactly once. Only a SHA-256 digest is
|
|
6
|
+
* persisted, so a database leak does not hand over working credentials.
|
|
7
|
+
*/
|
|
8
|
+
const BASE62 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
9
|
+
/** User codes avoid 0/O/1/I/L: they are read aloud and typed by hand. */
|
|
10
|
+
const USER_CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
|
|
11
|
+
function base62(bytes) {
|
|
12
|
+
let out = '';
|
|
13
|
+
for (const byte of bytes) {
|
|
14
|
+
out += BASE62[byte % 62];
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
export function generateToken(prefix) {
|
|
19
|
+
const token = `${prefix}_${base62(randomBytes(32))}`;
|
|
20
|
+
return { token, tokenHash: hashToken(token) };
|
|
21
|
+
}
|
|
22
|
+
export function hashToken(token) {
|
|
23
|
+
return createHash('sha256').update(token, 'utf8').digest('hex');
|
|
24
|
+
}
|
|
25
|
+
/** `WXYZ-1234`, from an alphabet chosen to survive being read over a phone. */
|
|
26
|
+
export function generateUserCode() {
|
|
27
|
+
const pick = (n) => {
|
|
28
|
+
const bytes = randomBytes(n);
|
|
29
|
+
let s = '';
|
|
30
|
+
for (const b of bytes)
|
|
31
|
+
s += USER_CODE_ALPHABET[b % USER_CODE_ALPHABET.length];
|
|
32
|
+
return s;
|
|
33
|
+
};
|
|
34
|
+
return `${pick(4)}-${pick(4)}`;
|
|
35
|
+
}
|
|
36
|
+
export function generateDeviceCode() {
|
|
37
|
+
return randomBytes(32).toString('hex');
|
|
38
|
+
}
|
|
39
|
+
/** Constant-time comparison for values an attacker can supply repeatedly. */
|
|
40
|
+
export function safeEqual(a, b) {
|
|
41
|
+
const bufA = Buffer.from(a, 'utf8');
|
|
42
|
+
const bufB = Buffer.from(b, 'utf8');
|
|
43
|
+
if (bufA.length !== bufB.length)
|
|
44
|
+
return false;
|
|
45
|
+
return timingSafeEqual(bufA, bufB);
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEvE;;;;;GAKG;AAEH,MAAM,MAAM,GAAG,gEAAgE,CAAC;AAEhF,yEAAyE;AACzE,MAAM,kBAAkB,GAAG,iCAAiC,CAAC;AAE7D,SAAS,MAAM,CAAC,KAAa;IAC3B,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,GAAG,IAAI,MAAM,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;IAC3B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,MAAM,KAAK,GAAG,GAAG,MAAM,IAAI,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;IACrD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAClE,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,gBAAgB;IAC9B,MAAM,IAAI,GAAG,CAAC,CAAS,EAAU,EAAE;QACjC,MAAM,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;QAC7B,IAAI,CAAC,GAAG,EAAE,CAAC;QACX,KAAK,MAAM,CAAC,IAAI,KAAK;YAAE,CAAC,IAAI,kBAAkB,CAAC,CAAC,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;QAC9E,OAAO,CAAC,CAAC;IACX,CAAC,CAAC;IACF,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACzC,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,SAAS,CAAC,CAAS,EAAE,CAAS;IAC5C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IACpC,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC9C,OAAO,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACrC,CAAC"}
|