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,75 @@
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.purgeExpiredAuthRateLimits = exports.clearAuthRateLimit = exports.consumeAuthRateLimit = exports.hashRateLimitIdentifier = 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 hashRateLimitIdentifier = (bucket, identifier) => (0, crypto_1.hashSecret)(`${bucket}:${identifier.trim().toLowerCase()}`);
17
+ exports.hashRateLimitIdentifier = hashRateLimitIdentifier;
18
+ /** Atomic fixed-window counter shared by all registry instances. */
19
+ const consumeAuthRateLimit = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
20
+ var _a;
21
+ if (!Number.isInteger(input.limit) || input.limit < 1)
22
+ throw new Error('Rate-limit limit must be a positive integer.');
23
+ if (!Number.isInteger(input.windowMs) || input.windowMs < 1) {
24
+ throw new Error('Rate-limit windowMs must be a positive integer.');
25
+ }
26
+ const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
27
+ const windowStartedAt = new Date(Math.floor(now.getTime() / input.windowMs) * input.windowMs);
28
+ const resetsAt = new Date(windowStartedAt.getTime() + input.windowMs);
29
+ const identifierHash = (0, exports.hashRateLimitIdentifier)(input.bucket, input.identifier);
30
+ const [counter] = yield db
31
+ .insert(schema_1.registryAuthRateLimits)
32
+ .values({
33
+ bucket: input.bucket,
34
+ identifierHash,
35
+ windowStartedAt,
36
+ attempts: 1,
37
+ expiresAt: resetsAt,
38
+ updatedAt: now,
39
+ })
40
+ .onConflictDoUpdate({
41
+ target: [schema_1.registryAuthRateLimits.bucket, schema_1.registryAuthRateLimits.identifierHash, schema_1.registryAuthRateLimits.windowStartedAt],
42
+ set: {
43
+ attempts: (0, drizzle_orm_1.sql) `${schema_1.registryAuthRateLimits.attempts} + 1`,
44
+ expiresAt: resetsAt,
45
+ updatedAt: now,
46
+ },
47
+ })
48
+ .returning({ attempts: schema_1.registryAuthRateLimits.attempts });
49
+ const attempts = counter.attempts;
50
+ return {
51
+ allowed: attempts <= input.limit,
52
+ attempts,
53
+ limit: input.limit,
54
+ remaining: Math.max(0, input.limit - attempts),
55
+ resetsAt,
56
+ retryAfterSeconds: attempts > input.limit ? Math.max(1, Math.ceil((resetsAt.getTime() - now.getTime()) / 1000)) : 0,
57
+ };
58
+ });
59
+ exports.consumeAuthRateLimit = consumeAuthRateLimit;
60
+ const clearAuthRateLimit = (db, bucket, identifier) => __awaiter(void 0, void 0, void 0, function* () {
61
+ const rows = yield db
62
+ .delete(schema_1.registryAuthRateLimits)
63
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryAuthRateLimits.bucket, bucket), (0, drizzle_orm_1.eq)(schema_1.registryAuthRateLimits.identifierHash, (0, exports.hashRateLimitIdentifier)(bucket, identifier))))
64
+ .returning({ identifierHash: schema_1.registryAuthRateLimits.identifierHash });
65
+ return rows.length;
66
+ });
67
+ exports.clearAuthRateLimit = clearAuthRateLimit;
68
+ const purgeExpiredAuthRateLimits = (db_1, ...args_1) => __awaiter(void 0, [db_1, ...args_1], void 0, function* (db, now = new Date()) {
69
+ const rows = yield db
70
+ .delete(schema_1.registryAuthRateLimits)
71
+ .where((0, drizzle_orm_1.lt)(schema_1.registryAuthRateLimits.expiresAt, now))
72
+ .returning({ identifierHash: schema_1.registryAuthRateLimits.identifierHash });
73
+ return rows.length;
74
+ });
75
+ exports.purgeExpiredAuthRateLimits = purgeExpiredAuthRateLimits;
@@ -0,0 +1,26 @@
1
+ import type { RegistryAccessScope, RegistryUserRole, RegistryUserStatus } from '../db/schema';
2
+ export type { RegistryAccessScope, RegistryUserRole, RegistryUserStatus };
3
+ export declare const REGISTRY_READ_SCOPE: "registry:read";
4
+ export declare const REGISTRY_WRITE_SCOPE: "registry:write";
5
+ export interface RegistryUser {
6
+ id: string;
7
+ email: string;
8
+ name: string | null;
9
+ image: string | null;
10
+ role: RegistryUserRole;
11
+ status: RegistryUserStatus;
12
+ emailVerifiedAt: Date | null;
13
+ authVersion: number;
14
+ createdAt: Date;
15
+ updatedAt: Date;
16
+ }
17
+ export interface RegistryPrincipal {
18
+ tokenId: string;
19
+ userId: string;
20
+ email: string;
21
+ name: string | null;
22
+ role: RegistryUserRole;
23
+ scopes: RegistryAccessScope[];
24
+ }
25
+ export declare const scopesForRegistryRole: (role: RegistryUserRole) => RegistryAccessScope[];
26
+ export declare const registryPrincipalHasScope: (principal: RegistryPrincipal, scope: RegistryAccessScope) => boolean;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registryPrincipalHasScope = exports.scopesForRegistryRole = exports.REGISTRY_WRITE_SCOPE = exports.REGISTRY_READ_SCOPE = void 0;
4
+ exports.REGISTRY_READ_SCOPE = 'registry:read';
5
+ exports.REGISTRY_WRITE_SCOPE = 'registry:write';
6
+ const scopesForRegistryRole = (role) => role === 'admin' ? [exports.REGISTRY_READ_SCOPE, exports.REGISTRY_WRITE_SCOPE] : [exports.REGISTRY_READ_SCOPE];
7
+ exports.scopesForRegistryRole = scopesForRegistryRole;
8
+ const registryPrincipalHasScope = (principal, scope) => principal.scopes.includes(scope);
9
+ exports.registryPrincipalHasScope = registryPrincipalHasScope;
@@ -0,0 +1,13 @@
1
+ import type { RegistryUser } from './types';
2
+ export declare const toRegistryUser: (row: {
3
+ id: string;
4
+ email: string;
5
+ name: string | null;
6
+ image: string | null;
7
+ role: RegistryUser["role"];
8
+ status: RegistryUser["status"];
9
+ emailVerifiedAt: Date | null;
10
+ authVersion: number;
11
+ createdAt: Date;
12
+ updatedAt: Date;
13
+ }) => RegistryUser;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toRegistryUser = void 0;
4
+ const toRegistryUser = (row) => ({
5
+ id: row.id,
6
+ email: row.email,
7
+ name: row.name,
8
+ image: row.image,
9
+ role: row.role,
10
+ status: row.status,
11
+ emailVerifiedAt: row.emailVerifiedAt,
12
+ authVersion: row.authVersion,
13
+ createdAt: row.createdAt,
14
+ updatedAt: row.updatedAt,
15
+ });
16
+ exports.toRegistryUser = toRegistryUser;
@@ -0,0 +1,31 @@
1
+ import type { RegistryDatabase } from '../db/client';
2
+ import { type RegistryUserRole } from '../db/schema';
3
+ import type { RegistryUser, RegistryUserStatus } from './types';
4
+ export type RegistryUserMutationResult = {
5
+ ok: true;
6
+ user: RegistryUser;
7
+ } | {
8
+ ok: false;
9
+ reason: 'not_found' | 'invalid_name' | 'invalid_image' | 'invalid_role' | 'last_admin' | 'self_deactivation' | 'invalid_status';
10
+ };
11
+ export declare const getRegistryUserById: (db: RegistryDatabase, userId: string) => Promise<RegistryUser | null>;
12
+ export declare const listRegistryUsers: (db: RegistryDatabase) => Promise<RegistryUser[]>;
13
+ /** Credentials authentication is intentionally enumeration-safe: all failures return null. */
14
+ export declare const authenticateRegistryCredentials: (db: RegistryDatabase, email: string, password: string) => Promise<RegistryUser | null>;
15
+ export declare const updateRegistryUserProfile: (db: RegistryDatabase, input: {
16
+ userId: string;
17
+ name: string;
18
+ image?: string | null;
19
+ now?: Date;
20
+ }) => Promise<RegistryUserMutationResult>;
21
+ export declare const updateRegistryUserRole: (db: RegistryDatabase, input: {
22
+ userId: string;
23
+ role: RegistryUserRole;
24
+ now?: Date;
25
+ }) => Promise<RegistryUserMutationResult>;
26
+ export declare const setRegistryUserStatus: (db: RegistryDatabase, input: {
27
+ userId: string;
28
+ status: Extract<RegistryUserStatus, "active" | "deactivated">;
29
+ actorUserId?: string;
30
+ now?: Date;
31
+ }) => Promise<RegistryUserMutationResult>;
@@ -0,0 +1,135 @@
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.setRegistryUserStatus = exports.updateRegistryUserRole = exports.updateRegistryUserProfile = exports.authenticateRegistryCredentials = exports.listRegistryUsers = exports.getRegistryUserById = void 0;
13
+ const drizzle_orm_1 = require("drizzle-orm");
14
+ const schema_1 = require("../db/schema");
15
+ const access_tokens_1 = require("./access-tokens");
16
+ const database_1 = require("./database");
17
+ const crypto_1 = require("./crypto");
18
+ const password_1 = require("./password");
19
+ const user_record_1 = require("./user-record");
20
+ const validation_1 = require("./validation");
21
+ const DUMMY_PASSWORD_HASH = 'scrypt-v1:AAAAAAAAAAAAAAAAAAAAAA:yK99vlYIaV57oeErq8gHqx18DxRfiUoHwETrTFUjt5JasWeMsu62stPgY-fO0TUeUc6uAu1hge_vuSQY15vvuQ';
22
+ const getRegistryUserById = (db, userId) => __awaiter(void 0, void 0, void 0, function* () {
23
+ const [row] = yield db.select().from(schema_1.registryUsers).where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, userId)).limit(1);
24
+ return row ? (0, user_record_1.toRegistryUser)(row) : null;
25
+ });
26
+ exports.getRegistryUserById = getRegistryUserById;
27
+ const listRegistryUsers = (db) => __awaiter(void 0, void 0, void 0, function* () {
28
+ const rows = yield db.select().from(schema_1.registryUsers).orderBy(schema_1.registryUsers.createdAt);
29
+ return rows.map(user_record_1.toRegistryUser);
30
+ });
31
+ exports.listRegistryUsers = listRegistryUsers;
32
+ /** Credentials authentication is intentionally enumeration-safe: all failures return null. */
33
+ const authenticateRegistryCredentials = (db, email, password) => __awaiter(void 0, void 0, void 0, function* () {
34
+ var _a;
35
+ const normalizedEmail = (0, crypto_1.normalizeEmail)(email);
36
+ const [row] = yield db.select().from(schema_1.registryUsers).where((0, drizzle_orm_1.eq)(schema_1.registryUsers.email, normalizedEmail)).limit(1);
37
+ const passwordMatches = yield (0, password_1.verifyPassword)(password, (_a = row === null || row === void 0 ? void 0 : row.passwordHash) !== null && _a !== void 0 ? _a : DUMMY_PASSWORD_HASH);
38
+ return row && row.status === 'active' && row.passwordHash && passwordMatches ? (0, user_record_1.toRegistryUser)(row) : null;
39
+ });
40
+ exports.authenticateRegistryCredentials = authenticateRegistryCredentials;
41
+ const updateRegistryUserProfile = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
42
+ var _a;
43
+ const name = (0, validation_1.validateRegistryDisplayName)(input.name);
44
+ if (!name)
45
+ return { ok: false, reason: 'invalid_name' };
46
+ const image = (0, validation_1.validateRegistryImageUrl)(input.image);
47
+ if (image === false)
48
+ return { ok: false, reason: 'invalid_image' };
49
+ const [row] = yield db
50
+ .update(schema_1.registryUsers)
51
+ .set({ name, image, updatedAt: (_a = input.now) !== null && _a !== void 0 ? _a : new Date() })
52
+ .where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, input.userId))
53
+ .returning();
54
+ return row ? { ok: true, user: (0, user_record_1.toRegistryUser)(row) } : { ok: false, reason: 'not_found' };
55
+ });
56
+ exports.updateRegistryUserProfile = updateRegistryUserProfile;
57
+ const updateRegistryUserRole = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
58
+ if (input.role !== 'admin' && input.role !== 'member')
59
+ return { ok: false, reason: 'invalid_role' };
60
+ return (0, database_1.withRegistryTransaction)(db, (tx) => __awaiter(void 0, void 0, void 0, function* () {
61
+ var _a;
62
+ yield (0, database_1.acquireRegistryAdvisoryLock)(tx, database_1.REGISTRY_ADMIN_LOCK_ID);
63
+ const [target] = yield tx.select().from(schema_1.registryUsers).where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, input.userId)).limit(1);
64
+ if (!target)
65
+ return { ok: false, reason: 'not_found' };
66
+ if (target.role === input.role)
67
+ return { ok: true, user: (0, user_record_1.toRegistryUser)(target) };
68
+ if (target.role === 'admin' && target.status === 'active' && input.role === 'member') {
69
+ const [{ value }] = yield tx
70
+ .select({ value: (0, drizzle_orm_1.count)() })
71
+ .from(schema_1.registryUsers)
72
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryUsers.role, 'admin'), (0, drizzle_orm_1.eq)(schema_1.registryUsers.status, 'active')));
73
+ if (Number(value) <= 1)
74
+ return { ok: false, reason: 'last_admin' };
75
+ }
76
+ const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
77
+ const [row] = yield tx
78
+ .update(schema_1.registryUsers)
79
+ .set({
80
+ role: input.role,
81
+ authVersion: (0, drizzle_orm_1.sql) `${schema_1.registryUsers.authVersion} + 1`,
82
+ updatedAt: now,
83
+ })
84
+ .where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, input.userId))
85
+ .returning();
86
+ if (target.role === 'admin' && input.role === 'member') {
87
+ yield (0, access_tokens_1.revokeAllRegistryAccessTokens)(tx, input.userId, now);
88
+ }
89
+ return { ok: true, user: (0, user_record_1.toRegistryUser)(row) };
90
+ }));
91
+ });
92
+ exports.updateRegistryUserRole = updateRegistryUserRole;
93
+ const setRegistryUserStatus = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
94
+ if (input.status !== 'active' && input.status !== 'deactivated') {
95
+ return { ok: false, reason: 'invalid_status' };
96
+ }
97
+ if (input.status === 'deactivated' && input.actorUserId === input.userId) {
98
+ return { ok: false, reason: 'self_deactivation' };
99
+ }
100
+ return (0, database_1.withRegistryTransaction)(db, (tx) => __awaiter(void 0, void 0, void 0, function* () {
101
+ var _a;
102
+ yield (0, database_1.acquireRegistryAdvisoryLock)(tx, database_1.REGISTRY_ADMIN_LOCK_ID);
103
+ const [target] = yield tx.select().from(schema_1.registryUsers).where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, input.userId)).limit(1);
104
+ if (!target)
105
+ return { ok: false, reason: 'not_found' };
106
+ if (target.status === input.status)
107
+ return { ok: true, user: (0, user_record_1.toRegistryUser)(target) };
108
+ if (input.status === 'active' && target.status === 'invited') {
109
+ return { ok: false, reason: 'invalid_status' };
110
+ }
111
+ if (target.role === 'admin' && target.status === 'active' && input.status === 'deactivated') {
112
+ const [{ value }] = yield tx
113
+ .select({ value: (0, drizzle_orm_1.count)() })
114
+ .from(schema_1.registryUsers)
115
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryUsers.role, 'admin'), (0, drizzle_orm_1.eq)(schema_1.registryUsers.status, 'active')));
116
+ if (Number(value) <= 1)
117
+ return { ok: false, reason: 'last_admin' };
118
+ }
119
+ const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
120
+ const [row] = yield tx
121
+ .update(schema_1.registryUsers)
122
+ .set({
123
+ status: input.status,
124
+ authVersion: (0, drizzle_orm_1.sql) `${schema_1.registryUsers.authVersion} + 1`,
125
+ updatedAt: now,
126
+ })
127
+ .where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, input.userId))
128
+ .returning();
129
+ if (input.status === 'deactivated') {
130
+ yield (0, access_tokens_1.revokeAllRegistryAccessTokens)(tx, input.userId, now);
131
+ }
132
+ return { ok: true, user: (0, user_record_1.toRegistryUser)(row) };
133
+ }));
134
+ });
135
+ exports.setRegistryUserStatus = setRegistryUserStatus;
@@ -0,0 +1,3 @@
1
+ export declare const validateRegistryEmail: (email: string) => string | null;
2
+ export declare const validateRegistryDisplayName: (name: string) => string | null;
3
+ export declare const validateRegistryImageUrl: (image: string | null | undefined) => string | null | false;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateRegistryImageUrl = exports.validateRegistryDisplayName = exports.validateRegistryEmail = void 0;
4
+ const crypto_1 = require("./crypto");
5
+ const SIMPLE_EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
6
+ const validateRegistryEmail = (email) => {
7
+ const normalized = (0, crypto_1.normalizeEmail)(email);
8
+ return normalized.length <= 320 && SIMPLE_EMAIL_PATTERN.test(normalized) ? normalized : null;
9
+ };
10
+ exports.validateRegistryEmail = validateRegistryEmail;
11
+ const validateRegistryDisplayName = (name) => {
12
+ const normalized = name.trim();
13
+ return normalized && normalized.length <= 120 ? normalized : null;
14
+ };
15
+ exports.validateRegistryDisplayName = validateRegistryDisplayName;
16
+ const validateRegistryImageUrl = (image) => {
17
+ const normalized = image === null || image === void 0 ? void 0 : image.trim();
18
+ if (!normalized)
19
+ return null;
20
+ try {
21
+ const parsed = new URL(normalized);
22
+ return parsed.protocol === 'https:' && normalized.length <= 2048 ? normalized : false;
23
+ }
24
+ catch (_a) {
25
+ return false;
26
+ }
27
+ };
28
+ exports.validateRegistryImageUrl = validateRegistryImageUrl;
@@ -62,7 +62,9 @@ const describeFetchFailure = (error, target, registryUrl) => {
62
62
  case 'runtime_mode_conflict':
63
63
  return `The registry at ${registryUrl} is not running in registry mode, so it cannot serve a checkout: ${error.message}`;
64
64
  case 'unauthorized':
65
- return `The registry rejected the access token (401). Check the configured access token matches the registry's token.`;
65
+ return `The registry rejected the access token (401). Run \`handoff-app login --url ${registryUrl}\` again, or replace the user-issued CI token.`;
66
+ case 'forbidden':
67
+ return `The registry token does not have permission to checkout assets (403). Authorize a token with registry:read access.`;
66
68
  default:
67
69
  return error.message;
68
70
  }
@@ -83,7 +85,7 @@ const wouldChange = (absolutePath, bytes) => {
83
85
  * Recreates workspace asset files; prompts before overwriting changed local files unless `--force`.
84
86
  */
85
87
  const checkoutAssets = (handoff, collection) => __awaiter(void 0, void 0, void 0, function* () {
86
- const connection = (0, index_1.resolveConnectionOrThrow)(handoff);
88
+ const connection = yield (0, index_1.resolveConnectionOrThrow)(handoff);
87
89
  if (collection && !(0, sets_1.isAssetCollection)(collection)) {
88
90
  throw new index_1.CheckoutError(`Unknown asset collection "${collection}". Supported collections: ${sets_1.ASSET_COLLECTIONS.join(', ')}.`);
89
91
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Connected-workspace checkout orchestration.
3
3
  *
4
- * `checkout <component|pattern> <id>` reads a normalized registry record and its registry-safe
4
+ * `checkout <components|patterns> <id>` reads a normalized registry record and its registry-safe
5
5
  * source files from the connected registry through the shared registry client, then writes them
6
6
  * into the local workspace in standard authoring form. The local **declaration is synthesized
7
7
  * locally** in the configured `runtime.workspace.declarationFormat` — declarations are never read
@@ -23,11 +23,16 @@ export declare class CheckoutError extends Error {
23
23
  * host has no local workspace to write into) and a resolved registry URL + access token. Throws an
24
24
  * actionable {@link CheckoutError} naming the exact misconfiguration.
25
25
  */
26
- export declare const resolveConnectionOrThrow: (handoff: Handoff) => import("../connection").ResolvedRegistryConnection;
26
+ export declare const resolveConnectionOrThrow: (handoff: Handoff) => Promise<import("../connection").ResolvedRegistryConnection>;
27
27
  /**
28
- * Checkout a single component or pattern from a connected workspace: precondition checks → fetch the
29
- * normalized record + source files → resolve the local target → explicit-overwrite guard → write
30
- * source files and synthesize the local declaration. Throws {@link CheckoutError} with actionable
31
- * messaging on any precondition or fetch failure.
28
+ * Checkout a single component, pattern, or page from the connected registry into this workspace.
29
+ * Overwriting existing local files requires `--force` or an interactive confirmation.
32
30
  */
33
31
  export declare const checkoutEntity: (handoff: Handoff, kind: TransferEntityKind, id: string) => Promise<void>;
32
+ /**
33
+ * Checkout every published component, pattern, or page of the kind into this workspace. Enumerates
34
+ * the registry's published ids, then checks each out through the shared client. A per-entity failure
35
+ * is collected and never aborts the rest; the run throws at the end if any entity failed. Overwrite
36
+ * prompting is per entity (skipped under `--force`).
37
+ */
38
+ export declare const checkoutEntities: (handoff: Handoff, kind: TransferEntityKind) => Promise<void>;