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
package/dist/index.d.ts CHANGED
@@ -45,17 +45,21 @@ declare class Handoff {
45
45
  */
46
46
  dbMigrate(): Promise<Handoff>;
47
47
  /**
48
- * Publish a single component, pattern, or page from this connected workspace to the configured
49
- * remote registry. Components and patterns receive a fresh targeted build before upload. Pages
50
- * upload their record and markdown source because they render at runtime. The publish module is
51
- * loaded lazily so the registry client and build code never enter the docs app bundle.
48
+ * Publish components, patterns, or pages from this connected workspace to the configured remote
49
+ * registry. With `id` it publishes that one entity after a targeted build; without `id` it builds
50
+ * the kind once and publishes every declared entity, skipping ones whose content is unchanged on
51
+ * the registry (`--force` re-uploads all). Pages upload their record and markdown source because
52
+ * they render at runtime. The publish module is loaded lazily so the registry client and build code
53
+ * never enter the docs app bundle.
52
54
  */
53
- publish(kind: 'component' | 'pattern' | 'page', id: string): Promise<Handoff>;
55
+ publish(kind: 'component' | 'pattern' | 'page', id?: string): Promise<Handoff>;
54
56
  /**
55
- * Checkout a single component, pattern, or page from the connected remote registry. The checkout
56
- * module is loaded lazily so the registry client never enters the docs app bundle.
57
+ * Checkout components, patterns, or pages from the connected remote registry into this workspace.
58
+ * With `id` it checks out that one entity; without `id` it checks out every published entity of the
59
+ * kind. Overwriting existing local files requires `--force` or an interactive confirmation. The
60
+ * checkout module is loaded lazily so the registry client never enters the docs app bundle.
57
61
  */
58
- checkout(kind: 'component' | 'pattern' | 'page', id: string): Promise<Handoff>;
62
+ checkout(kind: 'component' | 'pattern' | 'page', id?: string): Promise<Handoff>;
59
63
  /**
60
64
  * Publish design token sets from this connected workspace to the configured remote registry. Runs a
61
65
  * fresh token build (Figma extract + style transformers), discovers the logical sets, and uploads
package/dist/index.js CHANGED
@@ -159,28 +159,44 @@ class Handoff {
159
159
  });
160
160
  }
161
161
  /**
162
- * Publish a single component, pattern, or page from this connected workspace to the configured
163
- * remote registry. Components and patterns receive a fresh targeted build before upload. Pages
164
- * upload their record and markdown source because they render at runtime. The publish module is
165
- * loaded lazily so the registry client and build code never enter the docs app bundle.
162
+ * Publish components, patterns, or pages from this connected workspace to the configured remote
163
+ * registry. With `id` it publishes that one entity after a targeted build; without `id` it builds
164
+ * the kind once and publishes every declared entity, skipping ones whose content is unchanged on
165
+ * the registry (`--force` re-uploads all). Pages upload their record and markdown source because
166
+ * they render at runtime. The publish module is loaded lazily so the registry client and build code
167
+ * never enter the docs app bundle.
166
168
  */
167
169
  publish(kind, id) {
168
170
  return __awaiter(this, void 0, void 0, function* () {
169
171
  this.preRunner();
170
- const { publishEntity } = yield Promise.resolve().then(() => __importStar(require('./registry/publish')));
171
- yield publishEntity(this, kind, id);
172
+ if (id) {
173
+ const { publishEntity } = yield Promise.resolve().then(() => __importStar(require('./registry/publish')));
174
+ yield publishEntity(this, kind, id);
175
+ }
176
+ else {
177
+ const { publishEntities } = yield Promise.resolve().then(() => __importStar(require('./registry/publish')));
178
+ yield publishEntities(this, kind);
179
+ }
172
180
  return this;
173
181
  });
174
182
  }
175
183
  /**
176
- * Checkout a single component, pattern, or page from the connected remote registry. The checkout
177
- * module is loaded lazily so the registry client never enters the docs app bundle.
184
+ * Checkout components, patterns, or pages from the connected remote registry into this workspace.
185
+ * With `id` it checks out that one entity; without `id` it checks out every published entity of the
186
+ * kind. Overwriting existing local files requires `--force` or an interactive confirmation. The
187
+ * checkout module is loaded lazily so the registry client never enters the docs app bundle.
178
188
  */
179
189
  checkout(kind, id) {
180
190
  return __awaiter(this, void 0, void 0, function* () {
181
191
  this.preRunner();
182
- const { checkoutEntity } = yield Promise.resolve().then(() => __importStar(require('./registry/checkout')));
183
- yield checkoutEntity(this, kind, id);
192
+ if (id) {
193
+ const { checkoutEntity } = yield Promise.resolve().then(() => __importStar(require('./registry/checkout')));
194
+ yield checkoutEntity(this, kind, id);
195
+ }
196
+ else {
197
+ const { checkoutEntities } = yield Promise.resolve().then(() => __importStar(require('./registry/checkout')));
198
+ yield checkoutEntities(this, kind);
199
+ }
184
200
  return this;
185
201
  });
186
202
  }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * `runtime.registry.assetStorage` selects the active provider for **new** uploads: the built-in
5
5
  * `database` inline default, the pre-packaged `vercel-blob` adapter, or a `custom` module. Mirrors
6
- * {@link import('../db/adapter')}: only provider selection, module location, non-secret options, and
6
+ * {@link import('../db/driver')}: only provider selection, module location, non-secret options, and
7
7
  * env-var *names* are ever persisted; secret *values* are read from `process.env` at request time.
8
8
  */
9
9
  import type { Config } from '../../types/config';
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * `runtime.registry.assetStorage` selects the active provider for **new** uploads: the built-in
6
6
  * `database` inline default, the pre-packaged `vercel-blob` adapter, or a `custom` module. Mirrors
7
- * {@link import('../db/adapter')}: only provider selection, module location, non-secret options, and
7
+ * {@link import('../db/driver')}: only provider selection, module location, non-secret options, and
8
8
  * env-var *names* are ever persisted; secret *values* are read from `process.env` at request time.
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,46 @@
1
+ import type { RegistryDatabase } from '../db/client';
2
+ import { type RegistryAccessScope } from '../db/schema';
3
+ import { type RegistryPrincipal, type RegistryUserRole } from './types';
4
+ export interface RegistryAccessTokenSummary {
5
+ id: string;
6
+ name: string;
7
+ scopes: RegistryAccessScope[];
8
+ expiresAt: Date;
9
+ lastUsedAt: Date | null;
10
+ revokedAt: Date | null;
11
+ createdAt: Date;
12
+ }
13
+ export type CreateRegistryAccessTokenResult = {
14
+ ok: true;
15
+ token: string;
16
+ record: RegistryAccessTokenSummary;
17
+ } | {
18
+ ok: false;
19
+ reason: 'user_not_found' | 'user_inactive' | 'scope_forbidden' | 'invalid_name' | 'invalid_expiry';
20
+ };
21
+ export declare const normalizeRegistryScopes: (scopes: readonly RegistryAccessScope[]) => RegistryAccessScope[];
22
+ export declare const registryRoleAllowsScopes: (role: RegistryUserRole, scopes: readonly RegistryAccessScope[]) => boolean;
23
+ export declare const formatRegistryAccessToken: (id: string, secret: string) => string;
24
+ export declare const parseRegistryAccessToken: (token: string) => {
25
+ id: string;
26
+ secret: string;
27
+ } | null;
28
+ export declare const createRegistryAccessToken: (db: RegistryDatabase, input: {
29
+ userId: string;
30
+ name: string;
31
+ scopes: RegistryAccessScope[];
32
+ expiresAt?: Date;
33
+ now?: Date;
34
+ }) => Promise<CreateRegistryAccessTokenResult>;
35
+ export declare const listRegistryAccessTokens: (db: RegistryDatabase, userId: string) => Promise<RegistryAccessTokenSummary[]>;
36
+ export declare const revokeRegistryAccessToken: (db: RegistryDatabase, input: {
37
+ tokenId: string;
38
+ userId: string;
39
+ now?: Date;
40
+ }) => Promise<boolean>;
41
+ export declare const revokeAllRegistryAccessTokens: (db: RegistryDatabase, userId: string, now?: Date) => Promise<number>;
42
+ /**
43
+ * Resolve an opaque bearer credential into a live principal. User state is joined on every call,
44
+ * making deactivation effective even if a defensive token-revocation write was interrupted.
45
+ */
46
+ export declare const authenticateRegistryAccessToken: (db: RegistryDatabase, token: string, now?: Date) => Promise<RegistryPrincipal | null>;
@@ -0,0 +1,165 @@
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.authenticateRegistryAccessToken = exports.revokeAllRegistryAccessTokens = exports.revokeRegistryAccessToken = exports.listRegistryAccessTokens = exports.createRegistryAccessToken = exports.parseRegistryAccessToken = exports.formatRegistryAccessToken = exports.registryRoleAllowsScopes = exports.normalizeRegistryScopes = void 0;
13
+ const drizzle_orm_1 = require("drizzle-orm");
14
+ const schema_1 = require("../db/schema");
15
+ const crypto_1 = require("./crypto");
16
+ const database_1 = require("./database");
17
+ const types_1 = require("./types");
18
+ const ACCESS_TOKEN_PREFIX = 'hnd';
19
+ const DEFAULT_ACCESS_TOKEN_LIFETIME_MS = 365 * 24 * 60 * 60 * 1000;
20
+ const isRegistryScope = (value) => value === types_1.REGISTRY_READ_SCOPE || value === types_1.REGISTRY_WRITE_SCOPE;
21
+ const normalizeRegistryScopes = (scopes) => {
22
+ const unique = new Set(scopes.filter(isRegistryScope));
23
+ return [types_1.REGISTRY_READ_SCOPE, types_1.REGISTRY_WRITE_SCOPE].filter((scope) => unique.has(scope));
24
+ };
25
+ exports.normalizeRegistryScopes = normalizeRegistryScopes;
26
+ const registryRoleAllowsScopes = (role, scopes) => role === 'admin' || !scopes.includes(types_1.REGISTRY_WRITE_SCOPE);
27
+ exports.registryRoleAllowsScopes = registryRoleAllowsScopes;
28
+ const formatRegistryAccessToken = (id, secret) => `${ACCESS_TOKEN_PREFIX}_${id}_${secret}`;
29
+ exports.formatRegistryAccessToken = formatRegistryAccessToken;
30
+ const parseRegistryAccessToken = (token) => {
31
+ const match = /^hnd_([A-Za-z0-9_-]{16})_([A-Za-z0-9_-]{43})$/.exec(token.trim());
32
+ return match ? { id: match[1], secret: match[2] } : null;
33
+ };
34
+ exports.parseRegistryAccessToken = parseRegistryAccessToken;
35
+ const createRegistryAccessTokenInTransaction = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
36
+ var _a, _b;
37
+ const name = input.name.trim();
38
+ if (!name || name.length > 120)
39
+ return { ok: false, reason: 'invalid_name' };
40
+ const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
41
+ const expiresAt = (_b = input.expiresAt) !== null && _b !== void 0 ? _b : new Date(now.getTime() + DEFAULT_ACCESS_TOKEN_LIFETIME_MS);
42
+ if (!(expiresAt instanceof Date) || !Number.isFinite(expiresAt.getTime()) || expiresAt <= now) {
43
+ return { ok: false, reason: 'invalid_expiry' };
44
+ }
45
+ const [user] = yield db
46
+ .select({ id: schema_1.registryUsers.id, role: schema_1.registryUsers.role, status: schema_1.registryUsers.status })
47
+ .from(schema_1.registryUsers)
48
+ .where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, input.userId))
49
+ .limit(1)
50
+ .for('update');
51
+ if (!user)
52
+ return { ok: false, reason: 'user_not_found' };
53
+ if (user.status !== 'active')
54
+ return { ok: false, reason: 'user_inactive' };
55
+ const scopes = (0, exports.normalizeRegistryScopes)(input.scopes);
56
+ if (!scopes.length)
57
+ scopes.push(types_1.REGISTRY_READ_SCOPE);
58
+ if (!(0, exports.registryRoleAllowsScopes)(user.role, scopes))
59
+ return { ok: false, reason: 'scope_forbidden' };
60
+ const id = (0, crypto_1.createOpaqueSecret)(12);
61
+ const secret = (0, crypto_1.createOpaqueSecret)(32);
62
+ const [record] = yield db
63
+ .insert(schema_1.registryAccessTokens)
64
+ .values({
65
+ id,
66
+ userId: user.id,
67
+ name,
68
+ secretHash: (0, crypto_1.hashSecret)(secret),
69
+ scopes,
70
+ expiresAt,
71
+ })
72
+ .returning({
73
+ id: schema_1.registryAccessTokens.id,
74
+ name: schema_1.registryAccessTokens.name,
75
+ scopes: schema_1.registryAccessTokens.scopes,
76
+ expiresAt: schema_1.registryAccessTokens.expiresAt,
77
+ lastUsedAt: schema_1.registryAccessTokens.lastUsedAt,
78
+ revokedAt: schema_1.registryAccessTokens.revokedAt,
79
+ createdAt: schema_1.registryAccessTokens.createdAt,
80
+ });
81
+ return { ok: true, token: (0, exports.formatRegistryAccessToken)(id, secret), record };
82
+ });
83
+ const createRegistryAccessToken = (db, input) => __awaiter(void 0, void 0, void 0, function* () { return (0, database_1.withRegistryTransaction)(db, (tx) => createRegistryAccessTokenInTransaction(tx, input)); });
84
+ exports.createRegistryAccessToken = createRegistryAccessToken;
85
+ const listRegistryAccessTokens = (db, userId) => __awaiter(void 0, void 0, void 0, function* () {
86
+ return db
87
+ .select({
88
+ id: schema_1.registryAccessTokens.id,
89
+ name: schema_1.registryAccessTokens.name,
90
+ scopes: schema_1.registryAccessTokens.scopes,
91
+ expiresAt: schema_1.registryAccessTokens.expiresAt,
92
+ lastUsedAt: schema_1.registryAccessTokens.lastUsedAt,
93
+ revokedAt: schema_1.registryAccessTokens.revokedAt,
94
+ createdAt: schema_1.registryAccessTokens.createdAt,
95
+ })
96
+ .from(schema_1.registryAccessTokens)
97
+ .where((0, drizzle_orm_1.eq)(schema_1.registryAccessTokens.userId, userId))
98
+ .orderBy(schema_1.registryAccessTokens.createdAt);
99
+ });
100
+ exports.listRegistryAccessTokens = listRegistryAccessTokens;
101
+ const revokeRegistryAccessToken = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
102
+ var _a;
103
+ const rows = yield db
104
+ .update(schema_1.registryAccessTokens)
105
+ .set({ revokedAt: (_a = input.now) !== null && _a !== void 0 ? _a : new Date() })
106
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryAccessTokens.id, input.tokenId), (0, drizzle_orm_1.eq)(schema_1.registryAccessTokens.userId, input.userId), (0, drizzle_orm_1.isNull)(schema_1.registryAccessTokens.revokedAt)))
107
+ .returning({ id: schema_1.registryAccessTokens.id });
108
+ return rows.length > 0;
109
+ });
110
+ exports.revokeRegistryAccessToken = revokeRegistryAccessToken;
111
+ const revokeAllRegistryAccessTokens = (db_1, userId_1, ...args_1) => __awaiter(void 0, [db_1, userId_1, ...args_1], void 0, function* (db, userId, now = new Date()) {
112
+ const rows = yield db
113
+ .update(schema_1.registryAccessTokens)
114
+ .set({ revokedAt: now })
115
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryAccessTokens.userId, userId), (0, drizzle_orm_1.isNull)(schema_1.registryAccessTokens.revokedAt)))
116
+ .returning({ id: schema_1.registryAccessTokens.id });
117
+ return rows.length;
118
+ });
119
+ exports.revokeAllRegistryAccessTokens = revokeAllRegistryAccessTokens;
120
+ /**
121
+ * Resolve an opaque bearer credential into a live principal. User state is joined on every call,
122
+ * making deactivation effective even if a defensive token-revocation write was interrupted.
123
+ */
124
+ const authenticateRegistryAccessToken = (db_1, token_1, ...args_1) => __awaiter(void 0, [db_1, token_1, ...args_1], void 0, function* (db, token, now = new Date()) {
125
+ const parsed = (0, exports.parseRegistryAccessToken)(token);
126
+ if (!parsed)
127
+ return null;
128
+ const [row] = yield db
129
+ .select({
130
+ tokenId: schema_1.registryAccessTokens.id,
131
+ secretHash: schema_1.registryAccessTokens.secretHash,
132
+ scopes: schema_1.registryAccessTokens.scopes,
133
+ expiresAt: schema_1.registryAccessTokens.expiresAt,
134
+ revokedAt: schema_1.registryAccessTokens.revokedAt,
135
+ userId: schema_1.registryUsers.id,
136
+ email: schema_1.registryUsers.email,
137
+ name: schema_1.registryUsers.name,
138
+ role: schema_1.registryUsers.role,
139
+ status: schema_1.registryUsers.status,
140
+ })
141
+ .from(schema_1.registryAccessTokens)
142
+ .innerJoin(schema_1.registryUsers, (0, drizzle_orm_1.eq)(schema_1.registryUsers.id, schema_1.registryAccessTokens.userId))
143
+ .where((0, drizzle_orm_1.eq)(schema_1.registryAccessTokens.id, parsed.id))
144
+ .limit(1);
145
+ if (!row || row.status !== 'active' || row.revokedAt || row.expiresAt <= now || !(0, crypto_1.secretHashMatches)(parsed.secret, row.secretHash)) {
146
+ return null;
147
+ }
148
+ const scopes = (0, exports.normalizeRegistryScopes)(Array.isArray(row.scopes) ? row.scopes : []);
149
+ const updated = yield db
150
+ .update(schema_1.registryAccessTokens)
151
+ .set({ lastUsedAt: now })
152
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryAccessTokens.id, row.tokenId), (0, drizzle_orm_1.isNull)(schema_1.registryAccessTokens.revokedAt), (0, drizzle_orm_1.gt)(schema_1.registryAccessTokens.expiresAt, now)))
153
+ .returning({ id: schema_1.registryAccessTokens.id });
154
+ if (!updated.length)
155
+ return null;
156
+ return {
157
+ tokenId: row.tokenId,
158
+ userId: row.userId,
159
+ email: row.email,
160
+ name: row.name,
161
+ role: row.role,
162
+ scopes,
163
+ };
164
+ });
165
+ exports.authenticateRegistryAccessToken = authenticateRegistryAccessToken;
@@ -0,0 +1,66 @@
1
+ import type { RegistryDatabase } from '../db/client';
2
+ import { type RegistryUserRole } from '../db/schema';
3
+ import type { RegistryUser } from './types';
4
+ export declare const INVITATION_LIFETIME_MS: number;
5
+ export declare const PASSWORD_RESET_LIFETIME_MS: number;
6
+ export type CreateUserInvitationResult = {
7
+ ok: true;
8
+ user: RegistryUser;
9
+ token: string;
10
+ expiresAt: Date;
11
+ } | {
12
+ ok: false;
13
+ reason: 'invalid_email' | 'invalid_name' | 'invalid_role' | 'email_exists';
14
+ };
15
+ export type ResendUserInvitationResult = {
16
+ ok: true;
17
+ user: RegistryUser;
18
+ token: string;
19
+ expiresAt: Date;
20
+ } | {
21
+ ok: false;
22
+ reason: 'not_found' | 'already_accepted' | 'deactivated';
23
+ };
24
+ export type ConsumeAuthActionResult = {
25
+ ok: true;
26
+ user: RegistryUser;
27
+ } | {
28
+ ok: false;
29
+ reason: 'invalid_or_expired_token' | 'invalid_password';
30
+ error?: string;
31
+ };
32
+ export declare const createUserInvitation: (db: RegistryDatabase, input: {
33
+ email: string;
34
+ name: string;
35
+ role: RegistryUserRole;
36
+ now?: Date;
37
+ expiresAt?: Date;
38
+ }) => Promise<CreateUserInvitationResult>;
39
+ export declare const resendUserInvitation: (db: RegistryDatabase, input: {
40
+ userId: string;
41
+ now?: Date;
42
+ expiresAt?: Date;
43
+ }) => Promise<ResendUserInvitationResult>;
44
+ export declare const acceptUserInvitation: (db: RegistryDatabase, input: {
45
+ token: string;
46
+ password: string;
47
+ now?: Date;
48
+ }) => Promise<ConsumeAuthActionResult>;
49
+ /**
50
+ * Enumeration-safe reset request. The token is returned only to the trusted mail-delivery caller;
51
+ * unknown/inactive emails return the same successful shape with a null token.
52
+ */
53
+ export declare const createPasswordReset: (db: RegistryDatabase, email: string, options?: {
54
+ now?: Date;
55
+ expiresAt?: Date;
56
+ }) => Promise<{
57
+ ok: true;
58
+ user: RegistryUser | null;
59
+ token: string | null;
60
+ expiresAt: Date | null;
61
+ }>;
62
+ export declare const resetPassword: (db: RegistryDatabase, input: {
63
+ token: string;
64
+ password: string;
65
+ now?: Date;
66
+ }) => Promise<ConsumeAuthActionResult>;
@@ -0,0 +1,178 @@
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.resetPassword = exports.createPasswordReset = exports.acceptUserInvitation = exports.resendUserInvitation = exports.createUserInvitation = exports.PASSWORD_RESET_LIFETIME_MS = exports.INVITATION_LIFETIME_MS = 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 password_1 = require("./password");
20
+ const user_record_1 = require("./user-record");
21
+ const validation_1 = require("./validation");
22
+ exports.INVITATION_LIFETIME_MS = 7 * 24 * 60 * 60 * 1000;
23
+ exports.PASSWORD_RESET_LIFETIME_MS = 60 * 60 * 1000;
24
+ const invalidateUnusedActionTokens = (db, userId, purpose, now) => __awaiter(void 0, void 0, void 0, function* () {
25
+ yield db
26
+ .update(schema_1.registryAuthActionTokens)
27
+ .set({ usedAt: now })
28
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryAuthActionTokens.userId, userId), (0, drizzle_orm_1.eq)(schema_1.registryAuthActionTokens.purpose, purpose), (0, drizzle_orm_1.isNull)(schema_1.registryAuthActionTokens.usedAt)));
29
+ });
30
+ const insertActionToken = (db, userId, purpose, expiresAt, now) => __awaiter(void 0, void 0, void 0, function* () {
31
+ const token = (0, crypto_1.createOpaqueSecret)(32);
32
+ yield db.insert(schema_1.registryAuthActionTokens).values({
33
+ id: (0, node_crypto_1.randomUUID)(),
34
+ userId,
35
+ purpose,
36
+ tokenHash: (0, crypto_1.hashSecret)(token),
37
+ expiresAt,
38
+ createdAt: now,
39
+ });
40
+ return token;
41
+ });
42
+ const createUserInvitation = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
43
+ var _a, _b;
44
+ const email = (0, validation_1.validateRegistryEmail)(input.email);
45
+ if (!email)
46
+ return { ok: false, reason: 'invalid_email' };
47
+ const name = (0, validation_1.validateRegistryDisplayName)(input.name);
48
+ if (!name)
49
+ return { ok: false, reason: 'invalid_name' };
50
+ if (input.role !== 'admin' && input.role !== 'member')
51
+ return { ok: false, reason: 'invalid_role' };
52
+ const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
53
+ const expiresAt = (_b = input.expiresAt) !== null && _b !== void 0 ? _b : new Date(now.getTime() + exports.INVITATION_LIFETIME_MS);
54
+ return (0, database_1.withRegistryTransaction)(db, (tx) => __awaiter(void 0, void 0, void 0, function* () {
55
+ const userId = (0, node_crypto_1.randomUUID)();
56
+ const rows = yield tx
57
+ .insert(schema_1.registryUsers)
58
+ .values({
59
+ id: userId,
60
+ email,
61
+ name,
62
+ role: input.role,
63
+ status: 'invited',
64
+ createdAt: now,
65
+ updatedAt: now,
66
+ })
67
+ .onConflictDoNothing()
68
+ .returning();
69
+ const user = rows[0];
70
+ if (!user)
71
+ return { ok: false, reason: 'email_exists' };
72
+ const token = yield insertActionToken(tx, userId, 'invite', expiresAt, now);
73
+ return { ok: true, user: (0, user_record_1.toRegistryUser)(user), token, expiresAt };
74
+ }));
75
+ });
76
+ exports.createUserInvitation = createUserInvitation;
77
+ const resendUserInvitation = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
78
+ return (0, database_1.withRegistryTransaction)(db, (tx) => __awaiter(void 0, void 0, void 0, function* () {
79
+ var _a, _b;
80
+ const [user] = yield tx.select().from(schema_1.registryUsers).where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, input.userId)).limit(1).for('update');
81
+ if (!user)
82
+ return { ok: false, reason: 'not_found' };
83
+ if (user.status === 'active')
84
+ return { ok: false, reason: 'already_accepted' };
85
+ if (user.status === 'deactivated')
86
+ return { ok: false, reason: 'deactivated' };
87
+ const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
88
+ const expiresAt = (_b = input.expiresAt) !== null && _b !== void 0 ? _b : new Date(now.getTime() + exports.INVITATION_LIFETIME_MS);
89
+ yield invalidateUnusedActionTokens(tx, user.id, 'invite', now);
90
+ const token = yield insertActionToken(tx, user.id, 'invite', expiresAt, now);
91
+ return { ok: true, user: (0, user_record_1.toRegistryUser)(user), token, expiresAt };
92
+ }));
93
+ });
94
+ exports.resendUserInvitation = resendUserInvitation;
95
+ const acceptUserInvitation = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
96
+ var _a;
97
+ const validation = (0, password_1.validatePassword)(input.password);
98
+ if ('error' in validation)
99
+ return { ok: false, reason: 'invalid_password', error: validation.error };
100
+ const passwordHash = yield (0, password_1.hashPassword)(input.password);
101
+ const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
102
+ return (0, database_1.withRegistryTransaction)(db, (tx) => __awaiter(void 0, void 0, void 0, function* () {
103
+ const [consumed] = yield tx
104
+ .update(schema_1.registryAuthActionTokens)
105
+ .set({ usedAt: now })
106
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryAuthActionTokens.tokenHash, (0, crypto_1.hashSecret)(input.token)), (0, drizzle_orm_1.eq)(schema_1.registryAuthActionTokens.purpose, 'invite'), (0, drizzle_orm_1.isNull)(schema_1.registryAuthActionTokens.usedAt), (0, drizzle_orm_1.gt)(schema_1.registryAuthActionTokens.expiresAt, now)))
107
+ .returning({ userId: schema_1.registryAuthActionTokens.userId });
108
+ if (!consumed)
109
+ return { ok: false, reason: 'invalid_or_expired_token' };
110
+ const [user] = yield tx
111
+ .update(schema_1.registryUsers)
112
+ .set({
113
+ passwordHash,
114
+ status: 'active',
115
+ emailVerifiedAt: now,
116
+ updatedAt: now,
117
+ })
118
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, consumed.userId), (0, drizzle_orm_1.eq)(schema_1.registryUsers.status, 'invited')))
119
+ .returning();
120
+ return user ? { ok: true, user: (0, user_record_1.toRegistryUser)(user) } : { ok: false, reason: 'invalid_or_expired_token' };
121
+ }));
122
+ });
123
+ exports.acceptUserInvitation = acceptUserInvitation;
124
+ /**
125
+ * Enumeration-safe reset request. The token is returned only to the trusted mail-delivery caller;
126
+ * unknown/inactive emails return the same successful shape with a null token.
127
+ */
128
+ const createPasswordReset = (db_1, email_1, ...args_1) => __awaiter(void 0, [db_1, email_1, ...args_1], void 0, function* (db, email, options = {}) {
129
+ var _a, _b;
130
+ const normalizedEmail = (0, crypto_1.normalizeEmail)(email);
131
+ const now = (_a = options.now) !== null && _a !== void 0 ? _a : new Date();
132
+ const expiresAt = (_b = options.expiresAt) !== null && _b !== void 0 ? _b : new Date(now.getTime() + exports.PASSWORD_RESET_LIFETIME_MS);
133
+ return (0, database_1.withRegistryTransaction)(db, (tx) => __awaiter(void 0, void 0, void 0, function* () {
134
+ const [user] = yield tx
135
+ .select()
136
+ .from(schema_1.registryUsers)
137
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryUsers.email, normalizedEmail), (0, drizzle_orm_1.eq)(schema_1.registryUsers.status, 'active')))
138
+ .limit(1)
139
+ .for('update');
140
+ if (!user)
141
+ return { ok: true, user: null, token: null, expiresAt: null };
142
+ yield invalidateUnusedActionTokens(tx, user.id, 'password_reset', now);
143
+ const token = yield insertActionToken(tx, user.id, 'password_reset', expiresAt, now);
144
+ return { ok: true, user: (0, user_record_1.toRegistryUser)(user), token, expiresAt };
145
+ }));
146
+ });
147
+ exports.createPasswordReset = createPasswordReset;
148
+ const resetPassword = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
149
+ var _a;
150
+ const validation = (0, password_1.validatePassword)(input.password);
151
+ if ('error' in validation)
152
+ return { ok: false, reason: 'invalid_password', error: validation.error };
153
+ const passwordHash = yield (0, password_1.hashPassword)(input.password);
154
+ const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
155
+ return (0, database_1.withRegistryTransaction)(db, (tx) => __awaiter(void 0, void 0, void 0, function* () {
156
+ const [consumed] = yield tx
157
+ .update(schema_1.registryAuthActionTokens)
158
+ .set({ usedAt: now })
159
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryAuthActionTokens.tokenHash, (0, crypto_1.hashSecret)(input.token)), (0, drizzle_orm_1.eq)(schema_1.registryAuthActionTokens.purpose, 'password_reset'), (0, drizzle_orm_1.isNull)(schema_1.registryAuthActionTokens.usedAt), (0, drizzle_orm_1.gt)(schema_1.registryAuthActionTokens.expiresAt, now)))
160
+ .returning({ userId: schema_1.registryAuthActionTokens.userId });
161
+ if (!consumed)
162
+ return { ok: false, reason: 'invalid_or_expired_token' };
163
+ const [user] = yield tx
164
+ .update(schema_1.registryUsers)
165
+ .set({
166
+ passwordHash,
167
+ authVersion: (0, drizzle_orm_1.sql) `${schema_1.registryUsers.authVersion} + 1`,
168
+ updatedAt: now,
169
+ })
170
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, consumed.userId), (0, drizzle_orm_1.eq)(schema_1.registryUsers.status, 'active')))
171
+ .returning();
172
+ if (!user)
173
+ return { ok: false, reason: 'invalid_or_expired_token' };
174
+ yield (0, access_tokens_1.revokeAllRegistryAccessTokens)(tx, user.id, now);
175
+ return { ok: true, user: (0, user_record_1.toRegistryUser)(user) };
176
+ }));
177
+ });
178
+ exports.resetPassword = resetPassword;
@@ -0,0 +1,4 @@
1
+ export declare const hashSecret: (secret: string) => string;
2
+ export declare const createOpaqueSecret: (byteLength?: number) => string;
3
+ export declare const secretHashMatches: (secret: string, expectedHash: string) => boolean;
4
+ export declare const normalizeEmail: (email: string) => string;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeEmail = exports.secretHashMatches = exports.createOpaqueSecret = exports.hashSecret = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const hashSecret = (secret) => (0, node_crypto_1.createHash)('sha256').update(secret, 'utf8').digest('hex');
6
+ exports.hashSecret = hashSecret;
7
+ const createOpaqueSecret = (byteLength = 32) => (0, node_crypto_1.randomBytes)(byteLength).toString('base64url');
8
+ exports.createOpaqueSecret = createOpaqueSecret;
9
+ const secretHashMatches = (secret, expectedHash) => {
10
+ const actual = Buffer.from((0, exports.hashSecret)(secret), 'hex');
11
+ const expected = Buffer.from(expectedHash, 'hex');
12
+ return actual.length === expected.length && (0, node_crypto_1.timingSafeEqual)(actual, expected);
13
+ };
14
+ exports.secretHashMatches = secretHashMatches;
15
+ const normalizeEmail = (email) => email.trim().toLowerCase();
16
+ exports.normalizeEmail = normalizeEmail;
@@ -0,0 +1,7 @@
1
+ import type { RegistryDatabase } from '../db/client';
2
+ export type RegistryTransaction = RegistryDatabase;
3
+ export declare const withRegistryTransaction: <T>(db: RegistryDatabase, callback: (tx: RegistryTransaction) => Promise<T>) => Promise<T>;
4
+ export declare const acquireRegistryAdvisoryLock: (tx: RegistryTransaction, lockId: number) => Promise<void>;
5
+ /** Stable, package-owned Postgres advisory-lock ids. */
6
+ export declare const REGISTRY_INSTALL_LOCK_ID = 1841111001;
7
+ export declare const REGISTRY_ADMIN_LOCK_ID = 1841111002;
@@ -0,0 +1,22 @@
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.REGISTRY_ADMIN_LOCK_ID = exports.REGISTRY_INSTALL_LOCK_ID = exports.acquireRegistryAdvisoryLock = exports.withRegistryTransaction = void 0;
13
+ const drizzle_orm_1 = require("drizzle-orm");
14
+ const withRegistryTransaction = (db, callback) => __awaiter(void 0, void 0, void 0, function* () { return db.transaction((tx) => callback(tx)); });
15
+ exports.withRegistryTransaction = withRegistryTransaction;
16
+ const acquireRegistryAdvisoryLock = (tx, lockId) => __awaiter(void 0, void 0, void 0, function* () {
17
+ yield tx.execute((0, drizzle_orm_1.sql) `select pg_advisory_xact_lock(${lockId})`);
18
+ });
19
+ exports.acquireRegistryAdvisoryLock = acquireRegistryAdvisoryLock;
20
+ /** Stable, package-owned Postgres advisory-lock ids. */
21
+ exports.REGISTRY_INSTALL_LOCK_ID = 1841111001;
22
+ exports.REGISTRY_ADMIN_LOCK_ID = 1841111002;