handoff-app 2.0.0-1 → 2.0.0-3

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.
Files changed (219) hide show
  1. package/README.md +170 -91
  2. package/config/docs/foundations/colors/guidelines.md +9 -0
  3. package/config/docs/foundations/effects/guidelines.md +9 -0
  4. package/config/docs/foundations/effects/resources.md +9 -0
  5. package/config/docs/foundations/grid.md +10 -0
  6. package/config/docs/foundations/icons/guidelines.md +9 -0
  7. package/config/docs/foundations/images/guidelines.md +9 -0
  8. package/config/docs/foundations/images/resources.md +9 -0
  9. package/config/docs/foundations/images.md +10 -0
  10. package/config/docs/foundations/logo/resources.md +9 -0
  11. package/config/docs/foundations/typography/guidelines.md +9 -0
  12. package/config/docs/foundations/typography/resources.md +9 -0
  13. package/config/docs/foundations.md +5 -3
  14. package/dist/app-builder/build.js +109 -49
  15. package/dist/app-builder/client-config.js +13 -12
  16. package/dist/app-builder/paths.js +7 -9
  17. package/dist/app-builder/static-export.js +6 -6
  18. package/dist/app-builder/vercel-output.js +2 -2
  19. package/dist/artifacts/url.d.ts +7 -7
  20. package/dist/artifacts/url.js +7 -7
  21. package/dist/cli/auth/browser.d.ts +2 -0
  22. package/dist/cli/auth/browser.js +33 -0
  23. package/dist/cli/auth/device.d.ts +11 -0
  24. package/dist/cli/auth/device.js +155 -0
  25. package/dist/cli/auth/store.d.ts +29 -0
  26. package/dist/cli/auth/store.js +146 -0
  27. package/dist/cli/create.js +48 -3
  28. package/dist/cli/scaffold.js +20 -154
  29. package/dist/commands/checkout/index.d.ts +6 -6
  30. package/dist/commands/checkout/index.js +8 -12
  31. package/dist/commands/index.js +4 -0
  32. package/dist/commands/login/index.d.ts +8 -0
  33. package/dist/commands/login/index.js +53 -0
  34. package/dist/commands/logout/index.d.ts +7 -0
  35. package/dist/commands/logout/index.js +54 -0
  36. package/dist/commands/publish/index.d.ts +6 -5
  37. package/dist/commands/publish/index.js +8 -11
  38. package/dist/commands/utils.d.ts +6 -0
  39. package/dist/commands/utils.js +9 -1
  40. package/dist/config/entries.d.ts +47 -0
  41. package/dist/config/entries.js +162 -0
  42. package/dist/config/index.d.ts +2 -0
  43. package/dist/config/index.js +5 -1
  44. package/dist/config/runtime.d.ts +9 -9
  45. package/dist/config/runtime.js +15 -15
  46. package/dist/index.d.ts +12 -8
  47. package/dist/index.js +26 -10
  48. package/dist/registry/asset-storage/resolve.d.ts +1 -1
  49. package/dist/registry/asset-storage/resolve.js +1 -1
  50. package/dist/registry/auth/access-tokens.d.ts +46 -0
  51. package/dist/registry/auth/access-tokens.js +165 -0
  52. package/dist/registry/auth/action-tokens.d.ts +66 -0
  53. package/dist/registry/auth/action-tokens.js +178 -0
  54. package/dist/registry/auth/crypto.d.ts +4 -0
  55. package/dist/registry/auth/crypto.js +16 -0
  56. package/dist/registry/auth/database.d.ts +7 -0
  57. package/dist/registry/auth/database.js +22 -0
  58. package/dist/registry/auth/device-authorizations.d.ts +51 -0
  59. package/dist/registry/auth/device-authorizations.js +170 -0
  60. package/dist/registry/auth/gravatar.d.ts +1 -0
  61. package/dist/registry/auth/gravatar.js +8 -0
  62. package/dist/registry/auth/index.d.ts +11 -0
  63. package/dist/registry/auth/index.js +72 -0
  64. package/dist/registry/auth/installation.d.ts +42 -0
  65. package/dist/registry/auth/installation.js +105 -0
  66. package/dist/registry/auth/password.d.ts +12 -0
  67. package/dist/registry/auth/password.js +58 -0
  68. package/dist/registry/auth/rate-limits.d.ts +22 -0
  69. package/dist/registry/auth/rate-limits.js +75 -0
  70. package/dist/registry/auth/types.d.ts +26 -0
  71. package/dist/registry/auth/types.js +9 -0
  72. package/dist/registry/auth/user-record.d.ts +13 -0
  73. package/dist/registry/auth/user-record.js +16 -0
  74. package/dist/registry/auth/users.d.ts +31 -0
  75. package/dist/registry/auth/users.js +135 -0
  76. package/dist/registry/auth/validation.d.ts +3 -0
  77. package/dist/registry/auth/validation.js +28 -0
  78. package/dist/registry/checkout/assets.js +4 -2
  79. package/dist/registry/checkout/index.d.ts +11 -6
  80. package/dist/registry/checkout/index.js +159 -30
  81. package/dist/registry/checkout/tokens.js +4 -2
  82. package/dist/registry/client.d.ts +2 -1
  83. package/dist/registry/client.js +8 -0
  84. package/dist/registry/connection.d.ts +9 -0
  85. package/dist/registry/connection.js +29 -1
  86. package/dist/registry/db/client.d.ts +9 -9
  87. package/dist/registry/db/client.js +7 -7
  88. package/dist/registry/db/driver.d.ts +46 -0
  89. package/dist/registry/db/{adapter.js → driver.js} +22 -19
  90. package/dist/registry/db/index.d.ts +2 -2
  91. package/dist/registry/db/index.js +15 -9
  92. package/dist/registry/db/migrate.d.ts +1 -1
  93. package/dist/registry/db/migrate.js +5 -5
  94. package/dist/registry/db/schema.d.ts +2521 -750
  95. package/dist/registry/db/schema.js +126 -1
  96. package/dist/registry/publish/assets.js +1 -1
  97. package/dist/registry/publish/errors.js +3 -3
  98. package/dist/registry/publish/index.d.ts +9 -2
  99. package/dist/registry/publish/index.js +96 -10
  100. package/dist/registry/publish/tokens.js +1 -1
  101. package/dist/registry/transfer.d.ts +13 -0
  102. package/dist/types/config.d.ts +11 -4
  103. package/docs/cli.md +4 -0
  104. package/drizzle/0005_add_registry_auth.sql +95 -0
  105. package/drizzle/meta/0005_snapshot.json +1926 -0
  106. package/drizzle/meta/_journal.json +8 -1
  107. package/drizzle.config.ts +1 -1
  108. package/package.json +5 -4
  109. package/src/app/components/Auth/AccountLayout.tsx +87 -0
  110. package/src/app/components/Auth/AuthControls.tsx +99 -0
  111. package/src/app/components/Auth/AuthShell.tsx +73 -0
  112. package/src/app/components/Auth/RegistryInstallGate.tsx +23 -0
  113. package/src/app/components/Auth/RegistrySessionProvider.tsx +15 -0
  114. package/src/app/components/Auth/api.ts +20 -0
  115. package/src/app/components/Foundations/DisplayLogo.tsx +3 -3
  116. package/src/app/components/Layout/Header.tsx +2 -0
  117. package/src/app/components/Navigation/MobileNav.tsx +2 -0
  118. package/src/app/lib/auth/api.ts +113 -0
  119. package/src/app/lib/auth/config.ts +175 -0
  120. package/src/app/lib/auth/email.ts +34 -0
  121. package/src/app/lib/docs-api/runtime-config.ts +10 -24
  122. package/src/app/lib/registry-api/auth.ts +29 -46
  123. package/src/app/lib/registry-api/errors.ts +2 -2
  124. package/src/app/lib/registry-api/handler.ts +19 -14
  125. package/src/app/lib/registry-api/token-transfer.ts +1 -1
  126. package/src/app/lib/registry-api/transfer.ts +31 -2
  127. package/src/app/lib/registry-connection.ts +2 -2
  128. package/src/app/next.config.mjs +13 -4
  129. package/src/app/pages/_app.tsx +10 -4
  130. package/src/app/pages/account/index.tsx +153 -0
  131. package/src/app/pages/account/tokens.tsx +230 -0
  132. package/src/app/pages/account/users.tsx +310 -0
  133. package/src/app/pages/api/account/index.ts +26 -0
  134. package/src/app/pages/api/account/tokens/[id].ts +17 -0
  135. package/src/app/pages/api/account/tokens/index.ts +44 -0
  136. package/src/app/pages/api/admin/users/[id]/[action].ts +89 -0
  137. package/src/app/pages/api/admin/users/index.ts +53 -0
  138. package/src/app/pages/api/auth/[...nextauth].ts +4 -0
  139. package/src/app/pages/api/auth/request-reset.ts +53 -0
  140. package/src/app/pages/api/auth/reset-password.ts +29 -0
  141. package/src/app/pages/api/install.ts +102 -0
  142. package/src/app/pages/api/oauth/device/approve.ts +23 -0
  143. package/src/app/pages/api/oauth/device/index.ts +41 -0
  144. package/src/app/pages/api/oauth/revoke.ts +24 -0
  145. package/src/app/pages/api/oauth/token.ts +31 -0
  146. package/src/app/pages/api/registry/health.ts +4 -3
  147. package/src/app/pages/api/registry/transfer/[kind]/[id].ts +3 -3
  148. package/src/app/pages/api/registry/transfer/[kind]/index.ts +21 -0
  149. package/src/app/pages/api/registry/transfer/assets/[collection].ts +2 -1
  150. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +2 -1
  151. package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +2 -2
  152. package/src/app/pages/cli/device.tsx +143 -0
  153. package/src/app/pages/foundations/icons/index.tsx +2 -2
  154. package/src/app/pages/foundations/icons/{[name] → library/[name]}/index.tsx +6 -6
  155. package/src/app/pages/install.tsx +336 -0
  156. package/src/app/pages/login.tsx +117 -0
  157. package/src/app/pages/reset-password.tsx +159 -0
  158. package/src/app/types/next-auth.d.ts +27 -0
  159. package/src/app-builder/build.ts +96 -59
  160. package/src/app-builder/client-config.ts +12 -11
  161. package/src/app-builder/paths.ts +7 -9
  162. package/src/app-builder/static-export.ts +6 -6
  163. package/src/app-builder/vercel-output.ts +2 -2
  164. package/src/artifacts/url.ts +7 -7
  165. package/src/cli/auth/browser.ts +29 -0
  166. package/src/cli/auth/device.ts +181 -0
  167. package/src/cli/auth/store.ts +137 -0
  168. package/src/cli/create.ts +58 -3
  169. package/src/cli/scaffold.ts +20 -197
  170. package/src/commands/checkout/index.ts +10 -15
  171. package/src/commands/index.ts +4 -0
  172. package/src/commands/login/index.ts +48 -0
  173. package/src/commands/logout/index.ts +51 -0
  174. package/src/commands/publish/index.ts +10 -14
  175. package/src/commands/utils.ts +12 -0
  176. package/src/config/entries.ts +175 -0
  177. package/src/config/index.ts +4 -0
  178. package/src/config/runtime.ts +15 -14
  179. package/src/index.ts +26 -12
  180. package/src/registry/asset-storage/resolve.ts +1 -1
  181. package/src/registry/auth/access-tokens.ts +198 -0
  182. package/src/registry/auth/action-tokens.ts +225 -0
  183. package/src/registry/auth/crypto.ts +13 -0
  184. package/src/registry/auth/database.ts +15 -0
  185. package/src/registry/auth/device-authorizations.ts +223 -0
  186. package/src/registry/auth/gravatar.ts +6 -0
  187. package/src/registry/auth/index.ts +89 -0
  188. package/src/registry/auth/installation.ts +117 -0
  189. package/src/registry/auth/password.ts +46 -0
  190. package/src/registry/auth/rate-limits.ts +84 -0
  191. package/src/registry/auth/types.ts +34 -0
  192. package/src/registry/auth/user-record.ts +25 -0
  193. package/src/registry/auth/users.ts +145 -0
  194. package/src/registry/auth/validation.ts +24 -0
  195. package/src/registry/checkout/assets.ts +4 -2
  196. package/src/registry/checkout/index.ts +182 -30
  197. package/src/registry/checkout/tokens.ts +4 -2
  198. package/src/registry/client.ts +7 -1
  199. package/src/registry/connection.ts +23 -0
  200. package/src/registry/db/client.ts +12 -12
  201. package/src/registry/db/{adapter.ts → driver.ts} +24 -21
  202. package/src/registry/db/index.ts +16 -4
  203. package/src/registry/db/migrate.ts +5 -5
  204. package/src/registry/db/schema.ts +174 -1
  205. package/src/registry/publish/assets.ts +1 -1
  206. package/src/registry/publish/errors.ts +3 -3
  207. package/src/registry/publish/index.ts +108 -10
  208. package/src/registry/publish/tokens.ts +1 -1
  209. package/src/registry/transfer.ts +14 -0
  210. package/src/types/config.ts +11 -4
  211. package/templates/default/README.md.tpl +58 -1
  212. package/templates/default/gitignore.tpl +19 -3
  213. package/templates/default/handoff.config.js.tpl +13 -0
  214. package/templates/default/handoff.config.ts.tpl +13 -0
  215. package/templates/default/handoff.config.with-examples.js.tpl +13 -0
  216. package/templates/default/handoff.config.with-examples.ts.tpl +13 -0
  217. package/templates/default/package.json.tpl +2 -1
  218. package/templates/default/vercel.json.tpl +4 -0
  219. package/dist/registry/db/adapter.d.ts +0 -43
@@ -0,0 +1,51 @@
1
+ import type { RegistryDatabase } from '../db/client';
2
+ import { type RegistryAccessTokenSummary } from './access-tokens';
3
+ import { type RegistryAccessScope, type RegistryUser } from './types';
4
+ export declare const DEVICE_AUTHORIZATION_LIFETIME_SECONDS: number;
5
+ export declare const DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS = 5;
6
+ export declare const generateRegistryUserCode: () => string;
7
+ export declare const normalizeRegistryUserCode: (value: string) => string;
8
+ export interface CreateRegistryDeviceAuthorizationResult {
9
+ deviceCode: string;
10
+ userCode: string;
11
+ expiresIn: number;
12
+ interval: number;
13
+ expiresAt: Date;
14
+ }
15
+ export declare const createRegistryDeviceAuthorization: (db: RegistryDatabase, options?: {
16
+ now?: Date;
17
+ expiresInSeconds?: number;
18
+ }) => Promise<CreateRegistryDeviceAuthorizationResult>;
19
+ export type RegistryDeviceApprovalResult = {
20
+ ok: true;
21
+ user: RegistryUser;
22
+ scopes: RegistryAccessScope[];
23
+ } | {
24
+ ok: false;
25
+ reason: 'invalid_or_expired_code' | 'user_inactive';
26
+ };
27
+ export declare const approveRegistryDeviceAuthorization: (db: RegistryDatabase, input: {
28
+ userCode: string;
29
+ userId: string;
30
+ now?: Date;
31
+ }) => Promise<RegistryDeviceApprovalResult>;
32
+ export declare const denyRegistryDeviceAuthorization: (db: RegistryDatabase, userCode: string, now?: Date) => Promise<boolean>;
33
+ export type ExchangeRegistryDeviceAuthorizationResult = {
34
+ ok: true;
35
+ accessToken: string;
36
+ tokenType: 'Bearer';
37
+ expiresIn: number;
38
+ scopes: RegistryAccessScope[];
39
+ user: RegistryUser;
40
+ record: RegistryAccessTokenSummary;
41
+ } | {
42
+ ok: false;
43
+ error: 'invalid_grant' | 'expired_token' | 'authorization_pending' | 'access_denied';
44
+ errorDescription: string;
45
+ };
46
+ export declare const exchangeRegistryDeviceAuthorization: (db: RegistryDatabase, input: {
47
+ deviceCode: string;
48
+ tokenName?: string;
49
+ now?: Date;
50
+ }) => Promise<ExchangeRegistryDeviceAuthorizationResult>;
51
+ export declare const purgeExpiredRegistryDeviceAuthorizations: (db: RegistryDatabase, now?: Date) => Promise<number>;
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.purgeExpiredRegistryDeviceAuthorizations = exports.exchangeRegistryDeviceAuthorization = exports.denyRegistryDeviceAuthorization = exports.approveRegistryDeviceAuthorization = exports.createRegistryDeviceAuthorization = exports.normalizeRegistryUserCode = exports.generateRegistryUserCode = exports.DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS = exports.DEVICE_AUTHORIZATION_LIFETIME_SECONDS = void 0;
13
+ const drizzle_orm_1 = require("drizzle-orm");
14
+ const node_crypto_1 = require("node:crypto");
15
+ const schema_1 = require("../db/schema");
16
+ const access_tokens_1 = require("./access-tokens");
17
+ const crypto_1 = require("./crypto");
18
+ const database_1 = require("./database");
19
+ const types_1 = require("./types");
20
+ const user_record_1 = require("./user-record");
21
+ const USER_CODE_ALPHABET = 'BCDFGHJKLMNPQRSTVWXYZ23456789';
22
+ exports.DEVICE_AUTHORIZATION_LIFETIME_SECONDS = 15 * 60;
23
+ exports.DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS = 5;
24
+ const randomUserCodeSegment = (length) => {
25
+ const bytes = (0, node_crypto_1.randomBytes)(length);
26
+ let result = '';
27
+ for (let index = 0; index < length; index += 1) {
28
+ result += USER_CODE_ALPHABET[bytes[index] % USER_CODE_ALPHABET.length];
29
+ }
30
+ return result;
31
+ };
32
+ const generateRegistryUserCode = () => `${randomUserCodeSegment(4)}-${randomUserCodeSegment(4)}`;
33
+ exports.generateRegistryUserCode = generateRegistryUserCode;
34
+ const normalizeRegistryUserCode = (value) => {
35
+ const compact = value.toUpperCase().replace(/[^A-Z0-9]/g, '');
36
+ return compact.length === 8 ? `${compact.slice(0, 4)}-${compact.slice(4)}` : value.trim().toUpperCase();
37
+ };
38
+ exports.normalizeRegistryUserCode = normalizeRegistryUserCode;
39
+ const createRegistryDeviceAuthorization = (db_1, ...args_1) => __awaiter(void 0, [db_1, ...args_1], void 0, function* (db, options = {}) {
40
+ var _a, _b;
41
+ const now = (_a = options.now) !== null && _a !== void 0 ? _a : new Date();
42
+ const expiresIn = (_b = options.expiresInSeconds) !== null && _b !== void 0 ? _b : exports.DEVICE_AUTHORIZATION_LIFETIME_SECONDS;
43
+ const deviceCode = (0, node_crypto_1.randomBytes)(32).toString('base64url');
44
+ const userCode = (0, exports.generateRegistryUserCode)();
45
+ const expiresAt = new Date(now.getTime() + expiresIn * 1000);
46
+ yield db.insert(schema_1.registryDeviceAuthorizations).values({
47
+ id: (0, node_crypto_1.randomUUID)(),
48
+ deviceCodeHash: (0, crypto_1.hashSecret)(deviceCode),
49
+ userCode,
50
+ status: 'pending',
51
+ scopes: [],
52
+ expiresAt,
53
+ createdAt: now,
54
+ });
55
+ return {
56
+ deviceCode,
57
+ userCode,
58
+ expiresIn,
59
+ interval: exports.DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS,
60
+ expiresAt,
61
+ };
62
+ });
63
+ exports.createRegistryDeviceAuthorization = createRegistryDeviceAuthorization;
64
+ const approveRegistryDeviceAuthorization = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
65
+ var _a;
66
+ const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
67
+ const [user] = yield db.select().from(schema_1.registryUsers).where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, input.userId)).limit(1);
68
+ if (!user || user.status !== 'active')
69
+ return { ok: false, reason: 'user_inactive' };
70
+ const scopes = (0, types_1.scopesForRegistryRole)(user.role);
71
+ const [authorization] = yield db
72
+ .update(schema_1.registryDeviceAuthorizations)
73
+ .set({
74
+ status: 'approved',
75
+ userId: user.id,
76
+ scopes,
77
+ approvedAt: now,
78
+ })
79
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryDeviceAuthorizations.userCode, (0, exports.normalizeRegistryUserCode)(input.userCode)), (0, drizzle_orm_1.eq)(schema_1.registryDeviceAuthorizations.status, 'pending'), (0, drizzle_orm_1.gt)(schema_1.registryDeviceAuthorizations.expiresAt, now)))
80
+ .returning({ id: schema_1.registryDeviceAuthorizations.id });
81
+ return authorization ? { ok: true, user: (0, user_record_1.toRegistryUser)(user), scopes } : { ok: false, reason: 'invalid_or_expired_code' };
82
+ });
83
+ exports.approveRegistryDeviceAuthorization = approveRegistryDeviceAuthorization;
84
+ const denyRegistryDeviceAuthorization = (db_1, userCode_1, ...args_1) => __awaiter(void 0, [db_1, userCode_1, ...args_1], void 0, function* (db, userCode, now = new Date()) {
85
+ const rows = yield db
86
+ .update(schema_1.registryDeviceAuthorizations)
87
+ .set({ status: 'denied' })
88
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryDeviceAuthorizations.userCode, (0, exports.normalizeRegistryUserCode)(userCode)), (0, drizzle_orm_1.eq)(schema_1.registryDeviceAuthorizations.status, 'pending'), (0, drizzle_orm_1.gt)(schema_1.registryDeviceAuthorizations.expiresAt, now)))
89
+ .returning({ id: schema_1.registryDeviceAuthorizations.id });
90
+ return rows.length > 0;
91
+ });
92
+ exports.denyRegistryDeviceAuthorization = denyRegistryDeviceAuthorization;
93
+ const exchangeRegistryDeviceAuthorization = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
94
+ var _a, _b;
95
+ const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
96
+ const tokenName = ((_b = input.tokenName) === null || _b === void 0 ? void 0 : _b.trim()) || 'Handoff CLI';
97
+ return (0, database_1.withRegistryTransaction)(db, (tx) => __awaiter(void 0, void 0, void 0, function* () {
98
+ const [authorization] = yield tx
99
+ .select()
100
+ .from(schema_1.registryDeviceAuthorizations)
101
+ .where((0, drizzle_orm_1.eq)(schema_1.registryDeviceAuthorizations.deviceCodeHash, (0, crypto_1.hashSecret)(input.deviceCode)))
102
+ .limit(1);
103
+ if (!authorization) {
104
+ return { ok: false, error: 'invalid_grant', errorDescription: 'Unknown device code.' };
105
+ }
106
+ if (authorization.expiresAt <= now) {
107
+ return { ok: false, error: 'expired_token', errorDescription: 'Device authorization expired.' };
108
+ }
109
+ if (authorization.status === 'pending') {
110
+ return {
111
+ ok: false,
112
+ error: 'authorization_pending',
113
+ errorDescription: 'The authorization request is still pending.',
114
+ };
115
+ }
116
+ if (authorization.status === 'denied') {
117
+ return { ok: false, error: 'access_denied', errorDescription: 'The authorization request was denied.' };
118
+ }
119
+ if (authorization.status === 'consumed') {
120
+ return { ok: false, error: 'invalid_grant', errorDescription: 'Device code has already been used.' };
121
+ }
122
+ if (!authorization.userId) {
123
+ return { ok: false, error: 'invalid_grant', errorDescription: 'Device authorization has no user.' };
124
+ }
125
+ const [consumed] = yield tx
126
+ .update(schema_1.registryDeviceAuthorizations)
127
+ .set({ status: 'consumed', consumedAt: now })
128
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryDeviceAuthorizations.id, authorization.id), (0, drizzle_orm_1.eq)(schema_1.registryDeviceAuthorizations.status, 'approved'), (0, drizzle_orm_1.gt)(schema_1.registryDeviceAuthorizations.expiresAt, now)))
129
+ .returning({ id: schema_1.registryDeviceAuthorizations.id });
130
+ if (!consumed) {
131
+ return { ok: false, error: 'invalid_grant', errorDescription: 'Device code has already been used.' };
132
+ }
133
+ const [user] = yield tx.select().from(schema_1.registryUsers).where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, authorization.userId)).limit(1);
134
+ if (!user || user.status !== 'active') {
135
+ return { ok: false, error: 'access_denied', errorDescription: 'The approving account is inactive.' };
136
+ }
137
+ let scopes = (0, access_tokens_1.normalizeRegistryScopes)(authorization.scopes);
138
+ if (!(0, access_tokens_1.registryRoleAllowsScopes)(user.role, scopes))
139
+ scopes = (0, types_1.scopesForRegistryRole)(user.role);
140
+ const expiresAt = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000);
141
+ const token = yield (0, access_tokens_1.createRegistryAccessToken)(tx, {
142
+ userId: user.id,
143
+ name: tokenName,
144
+ scopes,
145
+ expiresAt,
146
+ now,
147
+ });
148
+ if (!token.ok) {
149
+ return { ok: false, error: 'access_denied', errorDescription: 'The account cannot issue this token.' };
150
+ }
151
+ return {
152
+ ok: true,
153
+ accessToken: token.token,
154
+ tokenType: 'Bearer',
155
+ expiresIn: Math.floor((expiresAt.getTime() - now.getTime()) / 1000),
156
+ scopes: token.record.scopes,
157
+ user: (0, user_record_1.toRegistryUser)(user),
158
+ record: token.record,
159
+ };
160
+ }));
161
+ });
162
+ exports.exchangeRegistryDeviceAuthorization = exchangeRegistryDeviceAuthorization;
163
+ const purgeExpiredRegistryDeviceAuthorizations = (db_1, ...args_1) => __awaiter(void 0, [db_1, ...args_1], void 0, function* (db, now = new Date()) {
164
+ const rows = yield db
165
+ .delete(schema_1.registryDeviceAuthorizations)
166
+ .where((0, drizzle_orm_1.lt)(schema_1.registryDeviceAuthorizations.expiresAt, now))
167
+ .returning({ id: schema_1.registryDeviceAuthorizations.id });
168
+ return rows.length;
169
+ });
170
+ exports.purgeExpiredRegistryDeviceAuthorizations = purgeExpiredRegistryDeviceAuthorizations;
@@ -0,0 +1 @@
1
+ export declare const buildGravatarUrl: (email: string) => string;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildGravatarUrl = void 0;
4
+ const crypto_1 = require("./crypto");
5
+ // Gravatar resolves an avatar from the SHA-256 hash of the trimmed, lowercased email.
6
+ // `d=identicon` guarantees a generated image renders even when the user has no Gravatar.
7
+ const buildGravatarUrl = (email) => `https://www.gravatar.com/avatar/${(0, crypto_1.hashSecret)((0, crypto_1.normalizeEmail)(email))}?d=identicon`;
8
+ exports.buildGravatarUrl = buildGravatarUrl;
@@ -0,0 +1,11 @@
1
+ export { createRegistryAccessToken, listRegistryAccessTokens, revokeRegistryAccessToken, revokeAllRegistryAccessTokens, authenticateRegistryAccessToken, formatRegistryAccessToken, parseRegistryAccessToken, normalizeRegistryScopes, registryRoleAllowsScopes, type RegistryAccessTokenSummary, type CreateRegistryAccessTokenResult, } from './access-tokens';
2
+ export { createUserInvitation, resendUserInvitation, acceptUserInvitation, createPasswordReset, resetPassword, INVITATION_LIFETIME_MS, PASSWORD_RESET_LIFETIME_MS, type CreateUserInvitationResult, type ResendUserInvitationResult, type ConsumeAuthActionResult, } from './action-tokens';
3
+ export { createOpaqueSecret, hashSecret, normalizeEmail, secretHashMatches } from './crypto';
4
+ export { buildGravatarUrl } from './gravatar';
5
+ export { createRegistryDeviceAuthorization, approveRegistryDeviceAuthorization, denyRegistryDeviceAuthorization, exchangeRegistryDeviceAuthorization, purgeExpiredRegistryDeviceAuthorizations, generateRegistryUserCode, normalizeRegistryUserCode, DEVICE_AUTHORIZATION_LIFETIME_SECONDS, DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS, type CreateRegistryDeviceAuthorizationResult, type RegistryDeviceApprovalResult, type ExchangeRegistryDeviceAuthorizationResult, } from './device-authorizations';
6
+ export { getRegistryInstallation, getRegistryInstallationState, installRegistry, REGISTRY_AUTH_SCHEMA_VERSION, REGISTRY_INSTALLATION_ID, type RegistryInstallation, type RegistryInstallationState, type RegistryInstallInput, type RegistryInstallResult, } from './installation';
7
+ export { hashPassword, verifyPassword, validatePassword, MINIMUM_PASSWORD_LENGTH, MAXIMUM_PASSWORD_LENGTH, type PasswordValidationResult, } from './password';
8
+ export { consumeAuthRateLimit, clearAuthRateLimit, purgeExpiredAuthRateLimits, hashRateLimitIdentifier, type ConsumeAuthRateLimitInput, type AuthRateLimitResult, } from './rate-limits';
9
+ export { getRegistryUserById, listRegistryUsers, authenticateRegistryCredentials, updateRegistryUserProfile, updateRegistryUserRole, setRegistryUserStatus, type RegistryUserMutationResult, } from './users';
10
+ export { REGISTRY_READ_SCOPE, REGISTRY_WRITE_SCOPE, scopesForRegistryRole, registryPrincipalHasScope, type RegistryAccessScope, type RegistryPrincipal, type RegistryUser, type RegistryUserRole, type RegistryUserStatus, } from './types';
11
+ export { validateRegistryDisplayName, validateRegistryEmail, validateRegistryImageUrl } from './validation';
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setRegistryUserStatus = exports.updateRegistryUserRole = exports.updateRegistryUserProfile = exports.authenticateRegistryCredentials = exports.listRegistryUsers = exports.getRegistryUserById = exports.hashRateLimitIdentifier = exports.purgeExpiredAuthRateLimits = exports.clearAuthRateLimit = exports.consumeAuthRateLimit = exports.MAXIMUM_PASSWORD_LENGTH = exports.MINIMUM_PASSWORD_LENGTH = exports.validatePassword = exports.verifyPassword = exports.hashPassword = exports.REGISTRY_INSTALLATION_ID = exports.REGISTRY_AUTH_SCHEMA_VERSION = exports.installRegistry = exports.getRegistryInstallationState = exports.getRegistryInstallation = exports.DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS = exports.DEVICE_AUTHORIZATION_LIFETIME_SECONDS = exports.normalizeRegistryUserCode = exports.generateRegistryUserCode = exports.purgeExpiredRegistryDeviceAuthorizations = exports.exchangeRegistryDeviceAuthorization = exports.denyRegistryDeviceAuthorization = exports.approveRegistryDeviceAuthorization = exports.createRegistryDeviceAuthorization = exports.buildGravatarUrl = exports.secretHashMatches = exports.normalizeEmail = exports.hashSecret = exports.createOpaqueSecret = exports.PASSWORD_RESET_LIFETIME_MS = exports.INVITATION_LIFETIME_MS = exports.resetPassword = exports.createPasswordReset = exports.acceptUserInvitation = exports.resendUserInvitation = exports.createUserInvitation = exports.registryRoleAllowsScopes = exports.normalizeRegistryScopes = exports.parseRegistryAccessToken = exports.formatRegistryAccessToken = exports.authenticateRegistryAccessToken = exports.revokeAllRegistryAccessTokens = exports.revokeRegistryAccessToken = exports.listRegistryAccessTokens = exports.createRegistryAccessToken = void 0;
4
+ exports.validateRegistryImageUrl = exports.validateRegistryEmail = exports.validateRegistryDisplayName = exports.registryPrincipalHasScope = exports.scopesForRegistryRole = exports.REGISTRY_WRITE_SCOPE = exports.REGISTRY_READ_SCOPE = void 0;
5
+ var access_tokens_1 = require("./access-tokens");
6
+ Object.defineProperty(exports, "createRegistryAccessToken", { enumerable: true, get: function () { return access_tokens_1.createRegistryAccessToken; } });
7
+ Object.defineProperty(exports, "listRegistryAccessTokens", { enumerable: true, get: function () { return access_tokens_1.listRegistryAccessTokens; } });
8
+ Object.defineProperty(exports, "revokeRegistryAccessToken", { enumerable: true, get: function () { return access_tokens_1.revokeRegistryAccessToken; } });
9
+ Object.defineProperty(exports, "revokeAllRegistryAccessTokens", { enumerable: true, get: function () { return access_tokens_1.revokeAllRegistryAccessTokens; } });
10
+ Object.defineProperty(exports, "authenticateRegistryAccessToken", { enumerable: true, get: function () { return access_tokens_1.authenticateRegistryAccessToken; } });
11
+ Object.defineProperty(exports, "formatRegistryAccessToken", { enumerable: true, get: function () { return access_tokens_1.formatRegistryAccessToken; } });
12
+ Object.defineProperty(exports, "parseRegistryAccessToken", { enumerable: true, get: function () { return access_tokens_1.parseRegistryAccessToken; } });
13
+ Object.defineProperty(exports, "normalizeRegistryScopes", { enumerable: true, get: function () { return access_tokens_1.normalizeRegistryScopes; } });
14
+ Object.defineProperty(exports, "registryRoleAllowsScopes", { enumerable: true, get: function () { return access_tokens_1.registryRoleAllowsScopes; } });
15
+ var action_tokens_1 = require("./action-tokens");
16
+ Object.defineProperty(exports, "createUserInvitation", { enumerable: true, get: function () { return action_tokens_1.createUserInvitation; } });
17
+ Object.defineProperty(exports, "resendUserInvitation", { enumerable: true, get: function () { return action_tokens_1.resendUserInvitation; } });
18
+ Object.defineProperty(exports, "acceptUserInvitation", { enumerable: true, get: function () { return action_tokens_1.acceptUserInvitation; } });
19
+ Object.defineProperty(exports, "createPasswordReset", { enumerable: true, get: function () { return action_tokens_1.createPasswordReset; } });
20
+ Object.defineProperty(exports, "resetPassword", { enumerable: true, get: function () { return action_tokens_1.resetPassword; } });
21
+ Object.defineProperty(exports, "INVITATION_LIFETIME_MS", { enumerable: true, get: function () { return action_tokens_1.INVITATION_LIFETIME_MS; } });
22
+ Object.defineProperty(exports, "PASSWORD_RESET_LIFETIME_MS", { enumerable: true, get: function () { return action_tokens_1.PASSWORD_RESET_LIFETIME_MS; } });
23
+ var crypto_1 = require("./crypto");
24
+ Object.defineProperty(exports, "createOpaqueSecret", { enumerable: true, get: function () { return crypto_1.createOpaqueSecret; } });
25
+ Object.defineProperty(exports, "hashSecret", { enumerable: true, get: function () { return crypto_1.hashSecret; } });
26
+ Object.defineProperty(exports, "normalizeEmail", { enumerable: true, get: function () { return crypto_1.normalizeEmail; } });
27
+ Object.defineProperty(exports, "secretHashMatches", { enumerable: true, get: function () { return crypto_1.secretHashMatches; } });
28
+ var gravatar_1 = require("./gravatar");
29
+ Object.defineProperty(exports, "buildGravatarUrl", { enumerable: true, get: function () { return gravatar_1.buildGravatarUrl; } });
30
+ var device_authorizations_1 = require("./device-authorizations");
31
+ Object.defineProperty(exports, "createRegistryDeviceAuthorization", { enumerable: true, get: function () { return device_authorizations_1.createRegistryDeviceAuthorization; } });
32
+ Object.defineProperty(exports, "approveRegistryDeviceAuthorization", { enumerable: true, get: function () { return device_authorizations_1.approveRegistryDeviceAuthorization; } });
33
+ Object.defineProperty(exports, "denyRegistryDeviceAuthorization", { enumerable: true, get: function () { return device_authorizations_1.denyRegistryDeviceAuthorization; } });
34
+ Object.defineProperty(exports, "exchangeRegistryDeviceAuthorization", { enumerable: true, get: function () { return device_authorizations_1.exchangeRegistryDeviceAuthorization; } });
35
+ Object.defineProperty(exports, "purgeExpiredRegistryDeviceAuthorizations", { enumerable: true, get: function () { return device_authorizations_1.purgeExpiredRegistryDeviceAuthorizations; } });
36
+ Object.defineProperty(exports, "generateRegistryUserCode", { enumerable: true, get: function () { return device_authorizations_1.generateRegistryUserCode; } });
37
+ Object.defineProperty(exports, "normalizeRegistryUserCode", { enumerable: true, get: function () { return device_authorizations_1.normalizeRegistryUserCode; } });
38
+ Object.defineProperty(exports, "DEVICE_AUTHORIZATION_LIFETIME_SECONDS", { enumerable: true, get: function () { return device_authorizations_1.DEVICE_AUTHORIZATION_LIFETIME_SECONDS; } });
39
+ Object.defineProperty(exports, "DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS", { enumerable: true, get: function () { return device_authorizations_1.DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS; } });
40
+ var installation_1 = require("./installation");
41
+ Object.defineProperty(exports, "getRegistryInstallation", { enumerable: true, get: function () { return installation_1.getRegistryInstallation; } });
42
+ Object.defineProperty(exports, "getRegistryInstallationState", { enumerable: true, get: function () { return installation_1.getRegistryInstallationState; } });
43
+ Object.defineProperty(exports, "installRegistry", { enumerable: true, get: function () { return installation_1.installRegistry; } });
44
+ Object.defineProperty(exports, "REGISTRY_AUTH_SCHEMA_VERSION", { enumerable: true, get: function () { return installation_1.REGISTRY_AUTH_SCHEMA_VERSION; } });
45
+ Object.defineProperty(exports, "REGISTRY_INSTALLATION_ID", { enumerable: true, get: function () { return installation_1.REGISTRY_INSTALLATION_ID; } });
46
+ var password_1 = require("./password");
47
+ Object.defineProperty(exports, "hashPassword", { enumerable: true, get: function () { return password_1.hashPassword; } });
48
+ Object.defineProperty(exports, "verifyPassword", { enumerable: true, get: function () { return password_1.verifyPassword; } });
49
+ Object.defineProperty(exports, "validatePassword", { enumerable: true, get: function () { return password_1.validatePassword; } });
50
+ Object.defineProperty(exports, "MINIMUM_PASSWORD_LENGTH", { enumerable: true, get: function () { return password_1.MINIMUM_PASSWORD_LENGTH; } });
51
+ Object.defineProperty(exports, "MAXIMUM_PASSWORD_LENGTH", { enumerable: true, get: function () { return password_1.MAXIMUM_PASSWORD_LENGTH; } });
52
+ var rate_limits_1 = require("./rate-limits");
53
+ Object.defineProperty(exports, "consumeAuthRateLimit", { enumerable: true, get: function () { return rate_limits_1.consumeAuthRateLimit; } });
54
+ Object.defineProperty(exports, "clearAuthRateLimit", { enumerable: true, get: function () { return rate_limits_1.clearAuthRateLimit; } });
55
+ Object.defineProperty(exports, "purgeExpiredAuthRateLimits", { enumerable: true, get: function () { return rate_limits_1.purgeExpiredAuthRateLimits; } });
56
+ Object.defineProperty(exports, "hashRateLimitIdentifier", { enumerable: true, get: function () { return rate_limits_1.hashRateLimitIdentifier; } });
57
+ var users_1 = require("./users");
58
+ Object.defineProperty(exports, "getRegistryUserById", { enumerable: true, get: function () { return users_1.getRegistryUserById; } });
59
+ Object.defineProperty(exports, "listRegistryUsers", { enumerable: true, get: function () { return users_1.listRegistryUsers; } });
60
+ Object.defineProperty(exports, "authenticateRegistryCredentials", { enumerable: true, get: function () { return users_1.authenticateRegistryCredentials; } });
61
+ Object.defineProperty(exports, "updateRegistryUserProfile", { enumerable: true, get: function () { return users_1.updateRegistryUserProfile; } });
62
+ Object.defineProperty(exports, "updateRegistryUserRole", { enumerable: true, get: function () { return users_1.updateRegistryUserRole; } });
63
+ Object.defineProperty(exports, "setRegistryUserStatus", { enumerable: true, get: function () { return users_1.setRegistryUserStatus; } });
64
+ var types_1 = require("./types");
65
+ Object.defineProperty(exports, "REGISTRY_READ_SCOPE", { enumerable: true, get: function () { return types_1.REGISTRY_READ_SCOPE; } });
66
+ Object.defineProperty(exports, "REGISTRY_WRITE_SCOPE", { enumerable: true, get: function () { return types_1.REGISTRY_WRITE_SCOPE; } });
67
+ Object.defineProperty(exports, "scopesForRegistryRole", { enumerable: true, get: function () { return types_1.scopesForRegistryRole; } });
68
+ Object.defineProperty(exports, "registryPrincipalHasScope", { enumerable: true, get: function () { return types_1.registryPrincipalHasScope; } });
69
+ var validation_1 = require("./validation");
70
+ Object.defineProperty(exports, "validateRegistryDisplayName", { enumerable: true, get: function () { return validation_1.validateRegistryDisplayName; } });
71
+ Object.defineProperty(exports, "validateRegistryEmail", { enumerable: true, get: function () { return validation_1.validateRegistryEmail; } });
72
+ Object.defineProperty(exports, "validateRegistryImageUrl", { enumerable: true, get: function () { return validation_1.validateRegistryImageUrl; } });
@@ -0,0 +1,42 @@
1
+ import type { RegistryDatabase } from '../db/client';
2
+ import { registryInstallations } from '../db/schema';
3
+ import type { RegistryUser } from './types';
4
+ /** Migration number that introduced the registry authentication schema. */
5
+ export declare const REGISTRY_AUTH_SCHEMA_VERSION = 5;
6
+ export declare const REGISTRY_INSTALLATION_ID = "default";
7
+ export type RegistryInstallation = typeof registryInstallations.$inferSelect;
8
+ export type RegistryInstallationState = {
9
+ status: 'ready';
10
+ installation: null;
11
+ userCount: 0;
12
+ } | {
13
+ status: 'installed';
14
+ installation: RegistryInstallation;
15
+ userCount: number;
16
+ } | {
17
+ status: 'inconsistent';
18
+ installation: null;
19
+ userCount: number;
20
+ };
21
+ export interface RegistryInstallInput {
22
+ email: string;
23
+ name: string;
24
+ password: string;
25
+ now?: Date;
26
+ }
27
+ export type RegistryInstallResult = {
28
+ ok: true;
29
+ user: RegistryUser;
30
+ installation: RegistryInstallation;
31
+ } | {
32
+ ok: false;
33
+ reason: 'invalid_email' | 'invalid_name' | 'invalid_password' | 'already_installed' | 'users_exist';
34
+ error?: string;
35
+ };
36
+ export declare const getRegistryInstallation: (db: RegistryDatabase) => Promise<RegistryInstallation | null>;
37
+ export declare const getRegistryInstallationState: (db: RegistryDatabase) => Promise<RegistryInstallationState>;
38
+ /**
39
+ * Complete first-visitor installation under a transaction-scoped advisory lock. Password hashing is
40
+ * intentionally performed before taking the lock; all state checks are repeated inside it.
41
+ */
42
+ export declare const installRegistry: (db: RegistryDatabase, input: RegistryInstallInput) => Promise<RegistryInstallResult>;
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.installRegistry = exports.getRegistryInstallationState = exports.getRegistryInstallation = exports.REGISTRY_INSTALLATION_ID = exports.REGISTRY_AUTH_SCHEMA_VERSION = void 0;
13
+ const drizzle_orm_1 = require("drizzle-orm");
14
+ const node_crypto_1 = require("node:crypto");
15
+ const schema_1 = require("../db/schema");
16
+ const database_1 = require("./database");
17
+ const password_1 = require("./password");
18
+ const user_record_1 = require("./user-record");
19
+ const validation_1 = require("./validation");
20
+ /** Migration number that introduced the registry authentication schema. */
21
+ exports.REGISTRY_AUTH_SCHEMA_VERSION = 5;
22
+ exports.REGISTRY_INSTALLATION_ID = 'default';
23
+ const getRegistryInstallation = (db) => __awaiter(void 0, void 0, void 0, function* () {
24
+ const [installation] = yield db
25
+ .select()
26
+ .from(schema_1.registryInstallations)
27
+ .where((0, drizzle_orm_1.eq)(schema_1.registryInstallations.id, exports.REGISTRY_INSTALLATION_ID))
28
+ .limit(1);
29
+ return installation !== null && installation !== void 0 ? installation : null;
30
+ });
31
+ exports.getRegistryInstallation = getRegistryInstallation;
32
+ const getRegistryInstallationState = (db) => __awaiter(void 0, void 0, void 0, function* () {
33
+ const installation = yield (0, exports.getRegistryInstallation)(db);
34
+ const [{ value }] = yield db.select({ value: (0, drizzle_orm_1.count)() }).from(schema_1.registryUsers);
35
+ const userCount = Number(value);
36
+ if (installation)
37
+ return { status: 'installed', installation, userCount };
38
+ return userCount === 0
39
+ ? { status: 'ready', installation: null, userCount: 0 }
40
+ : { status: 'inconsistent', installation: null, userCount };
41
+ });
42
+ exports.getRegistryInstallationState = getRegistryInstallationState;
43
+ /**
44
+ * Complete first-visitor installation under a transaction-scoped advisory lock. Password hashing is
45
+ * intentionally performed before taking the lock; all state checks are repeated inside it.
46
+ */
47
+ const installRegistry = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
48
+ var _a;
49
+ const email = (0, validation_1.validateRegistryEmail)(input.email);
50
+ if (!email)
51
+ return { ok: false, reason: 'invalid_email' };
52
+ const name = (0, validation_1.validateRegistryDisplayName)(input.name);
53
+ if (!name)
54
+ return { ok: false, reason: 'invalid_name' };
55
+ const passwordValidation = (0, password_1.validatePassword)(input.password);
56
+ if ('error' in passwordValidation) {
57
+ return { ok: false, reason: 'invalid_password', error: passwordValidation.error };
58
+ }
59
+ const passwordHash = yield (0, password_1.hashPassword)(input.password);
60
+ const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
61
+ return (0, database_1.withRegistryTransaction)(db, (tx) => __awaiter(void 0, void 0, void 0, function* () {
62
+ yield (0, database_1.acquireRegistryAdvisoryLock)(tx, database_1.REGISTRY_INSTALL_LOCK_ID);
63
+ const [installation] = yield tx
64
+ .select()
65
+ .from(schema_1.registryInstallations)
66
+ .where((0, drizzle_orm_1.eq)(schema_1.registryInstallations.id, exports.REGISTRY_INSTALLATION_ID))
67
+ .limit(1);
68
+ if (installation)
69
+ return { ok: false, reason: 'already_installed' };
70
+ const [{ value }] = yield tx.select({ value: (0, drizzle_orm_1.count)() }).from(schema_1.registryUsers);
71
+ if (Number(value) > 0)
72
+ return { ok: false, reason: 'users_exist' };
73
+ const userId = (0, node_crypto_1.randomUUID)();
74
+ const [userRow] = yield tx
75
+ .insert(schema_1.registryUsers)
76
+ .values({
77
+ id: userId,
78
+ email,
79
+ name,
80
+ role: 'admin',
81
+ status: 'active',
82
+ passwordHash,
83
+ emailVerifiedAt: now,
84
+ createdAt: now,
85
+ updatedAt: now,
86
+ })
87
+ .returning();
88
+ const [createdInstallation] = yield tx
89
+ .insert(schema_1.registryInstallations)
90
+ .values({
91
+ id: exports.REGISTRY_INSTALLATION_ID,
92
+ status: 'installed',
93
+ schemaVersion: exports.REGISTRY_AUTH_SCHEMA_VERSION,
94
+ initialAdminUserId: userId,
95
+ installedAt: now,
96
+ })
97
+ .returning();
98
+ return {
99
+ ok: true,
100
+ user: (0, user_record_1.toRegistryUser)(userRow),
101
+ installation: createdInstallation,
102
+ };
103
+ }));
104
+ });
105
+ exports.installRegistry = installRegistry;
@@ -0,0 +1,12 @@
1
+ export declare const MINIMUM_PASSWORD_LENGTH = 12;
2
+ export declare const MAXIMUM_PASSWORD_LENGTH = 1024;
3
+ export type PasswordValidationResult = {
4
+ valid: true;
5
+ } | {
6
+ valid: false;
7
+ error: string;
8
+ };
9
+ export declare const validatePassword: (password: string) => PasswordValidationResult;
10
+ /** Versioned scrypt encoding: `scrypt-v1:<base64url salt>:<base64url derived key>`. */
11
+ export declare const hashPassword: (password: string) => Promise<string>;
12
+ export declare const verifyPassword: (password: string, encoded: string | null | undefined) => Promise<boolean>;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.verifyPassword = exports.hashPassword = exports.validatePassword = exports.MAXIMUM_PASSWORD_LENGTH = exports.MINIMUM_PASSWORD_LENGTH = void 0;
13
+ const node_crypto_1 = require("node:crypto");
14
+ const node_util_1 = require("node:util");
15
+ const scryptAsync = (0, node_util_1.promisify)(node_crypto_1.scrypt);
16
+ const PASSWORD_HASH_VERSION = 'scrypt-v1';
17
+ const PASSWORD_KEY_LENGTH = 64;
18
+ exports.MINIMUM_PASSWORD_LENGTH = 12;
19
+ exports.MAXIMUM_PASSWORD_LENGTH = 1024;
20
+ const validatePassword = (password) => {
21
+ if (password.length < exports.MINIMUM_PASSWORD_LENGTH) {
22
+ return { valid: false, error: `Password must be at least ${exports.MINIMUM_PASSWORD_LENGTH} characters.` };
23
+ }
24
+ if (password.length > exports.MAXIMUM_PASSWORD_LENGTH) {
25
+ return { valid: false, error: `Password must be at most ${exports.MAXIMUM_PASSWORD_LENGTH} characters.` };
26
+ }
27
+ return { valid: true };
28
+ };
29
+ exports.validatePassword = validatePassword;
30
+ /** Versioned scrypt encoding: `scrypt-v1:<base64url salt>:<base64url derived key>`. */
31
+ const hashPassword = (password) => __awaiter(void 0, void 0, void 0, function* () {
32
+ const validation = (0, exports.validatePassword)(password);
33
+ if ('error' in validation)
34
+ throw new Error(validation.error);
35
+ const salt = (0, node_crypto_1.randomBytes)(16);
36
+ const derived = (yield scryptAsync(password, salt, PASSWORD_KEY_LENGTH));
37
+ return `${PASSWORD_HASH_VERSION}:${salt.toString('base64url')}:${derived.toString('base64url')}`;
38
+ });
39
+ exports.hashPassword = hashPassword;
40
+ const verifyPassword = (password, encoded) => __awaiter(void 0, void 0, void 0, function* () {
41
+ if (!encoded || password.length > exports.MAXIMUM_PASSWORD_LENGTH)
42
+ return false;
43
+ const [version, saltText, hashText, ...rest] = encoded.split(':');
44
+ if (version !== PASSWORD_HASH_VERSION || !saltText || !hashText || rest.length)
45
+ return false;
46
+ try {
47
+ const salt = Buffer.from(saltText, 'base64url');
48
+ const expected = Buffer.from(hashText, 'base64url');
49
+ if (salt.length !== 16 || expected.length !== PASSWORD_KEY_LENGTH)
50
+ return false;
51
+ const actual = (yield scryptAsync(password, salt, PASSWORD_KEY_LENGTH));
52
+ return actual.length === expected.length && (0, node_crypto_1.timingSafeEqual)(actual, expected);
53
+ }
54
+ catch (_a) {
55
+ return false;
56
+ }
57
+ });
58
+ exports.verifyPassword = verifyPassword;
@@ -0,0 +1,22 @@
1
+ import type { RegistryDatabase } from '../db/client';
2
+ import { type RegistryRateLimitBucket } from '../db/schema';
3
+ export interface ConsumeAuthRateLimitInput {
4
+ bucket: RegistryRateLimitBucket;
5
+ identifier: string;
6
+ limit: number;
7
+ windowMs: number;
8
+ now?: Date;
9
+ }
10
+ export interface AuthRateLimitResult {
11
+ allowed: boolean;
12
+ attempts: number;
13
+ limit: number;
14
+ remaining: number;
15
+ resetsAt: Date;
16
+ retryAfterSeconds: number;
17
+ }
18
+ export declare const hashRateLimitIdentifier: (bucket: RegistryRateLimitBucket, identifier: string) => string;
19
+ /** Atomic fixed-window counter shared by all registry instances. */
20
+ export declare const consumeAuthRateLimit: (db: RegistryDatabase, input: ConsumeAuthRateLimitInput) => Promise<AuthRateLimitResult>;
21
+ export declare const clearAuthRateLimit: (db: RegistryDatabase, bucket: RegistryRateLimitBucket, identifier: string) => Promise<number>;
22
+ export declare const purgeExpiredAuthRateLimits: (db: RegistryDatabase, now?: Date) => Promise<number>;