handoff-app 2.0.0-2 → 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 (161) hide show
  1. package/README.md +140 -83
  2. package/dist/app-builder/build.js +21 -4
  3. package/dist/app-builder/client-config.js +9 -8
  4. package/dist/cli/auth/browser.d.ts +2 -0
  5. package/dist/cli/auth/browser.js +33 -0
  6. package/dist/cli/auth/device.d.ts +11 -0
  7. package/dist/cli/auth/device.js +155 -0
  8. package/dist/cli/auth/store.d.ts +29 -0
  9. package/dist/cli/auth/store.js +146 -0
  10. package/dist/cli/scaffold.js +20 -154
  11. package/dist/commands/index.js +4 -0
  12. package/dist/commands/login/index.d.ts +8 -0
  13. package/dist/commands/login/index.js +53 -0
  14. package/dist/commands/logout/index.d.ts +7 -0
  15. package/dist/commands/logout/index.js +54 -0
  16. package/dist/config/entries.d.ts +47 -0
  17. package/dist/config/entries.js +162 -0
  18. package/dist/config/index.d.ts +2 -0
  19. package/dist/config/index.js +5 -1
  20. package/dist/config/runtime.d.ts +9 -9
  21. package/dist/config/runtime.js +15 -15
  22. package/dist/registry/auth/access-tokens.d.ts +46 -0
  23. package/dist/registry/auth/access-tokens.js +165 -0
  24. package/dist/registry/auth/action-tokens.d.ts +66 -0
  25. package/dist/registry/auth/action-tokens.js +178 -0
  26. package/dist/registry/auth/crypto.d.ts +4 -0
  27. package/dist/registry/auth/crypto.js +16 -0
  28. package/dist/registry/auth/database.d.ts +7 -0
  29. package/dist/registry/auth/database.js +22 -0
  30. package/dist/registry/auth/device-authorizations.d.ts +51 -0
  31. package/dist/registry/auth/device-authorizations.js +170 -0
  32. package/dist/registry/auth/gravatar.d.ts +1 -0
  33. package/dist/registry/auth/gravatar.js +8 -0
  34. package/dist/registry/auth/index.d.ts +11 -0
  35. package/dist/registry/auth/index.js +72 -0
  36. package/dist/registry/auth/installation.d.ts +42 -0
  37. package/dist/registry/auth/installation.js +105 -0
  38. package/dist/registry/auth/password.d.ts +12 -0
  39. package/dist/registry/auth/password.js +58 -0
  40. package/dist/registry/auth/rate-limits.d.ts +22 -0
  41. package/dist/registry/auth/rate-limits.js +75 -0
  42. package/dist/registry/auth/types.d.ts +26 -0
  43. package/dist/registry/auth/types.js +9 -0
  44. package/dist/registry/auth/user-record.d.ts +13 -0
  45. package/dist/registry/auth/user-record.js +16 -0
  46. package/dist/registry/auth/users.d.ts +31 -0
  47. package/dist/registry/auth/users.js +135 -0
  48. package/dist/registry/auth/validation.d.ts +3 -0
  49. package/dist/registry/auth/validation.js +28 -0
  50. package/dist/registry/checkout/assets.js +4 -2
  51. package/dist/registry/checkout/index.d.ts +1 -1
  52. package/dist/registry/checkout/index.js +101 -23
  53. package/dist/registry/checkout/tokens.js +4 -2
  54. package/dist/registry/connection.d.ts +9 -0
  55. package/dist/registry/connection.js +29 -1
  56. package/dist/registry/db/driver.d.ts +5 -4
  57. package/dist/registry/db/driver.js +5 -4
  58. package/dist/registry/db/index.d.ts +1 -1
  59. package/dist/registry/db/index.js +7 -1
  60. package/dist/registry/db/schema.d.ts +2521 -750
  61. package/dist/registry/db/schema.js +126 -1
  62. package/dist/registry/publish/assets.js +1 -1
  63. package/dist/registry/publish/errors.js +3 -3
  64. package/dist/registry/publish/index.d.ts +1 -1
  65. package/dist/registry/publish/index.js +9 -9
  66. package/dist/registry/publish/tokens.js +1 -1
  67. package/dist/types/config.d.ts +4 -1
  68. package/docs/cli.md +4 -0
  69. package/drizzle/0005_add_registry_auth.sql +95 -0
  70. package/drizzle/meta/0005_snapshot.json +1926 -0
  71. package/drizzle/meta/_journal.json +8 -1
  72. package/package.json +5 -4
  73. package/src/app/components/Auth/AccountLayout.tsx +87 -0
  74. package/src/app/components/Auth/AuthControls.tsx +99 -0
  75. package/src/app/components/Auth/AuthShell.tsx +73 -0
  76. package/src/app/components/Auth/RegistryInstallGate.tsx +23 -0
  77. package/src/app/components/Auth/RegistrySessionProvider.tsx +15 -0
  78. package/src/app/components/Auth/api.ts +20 -0
  79. package/src/app/components/Layout/Header.tsx +2 -0
  80. package/src/app/components/Navigation/MobileNav.tsx +2 -0
  81. package/src/app/lib/auth/api.ts +113 -0
  82. package/src/app/lib/auth/config.ts +175 -0
  83. package/src/app/lib/auth/email.ts +34 -0
  84. package/src/app/lib/docs-api/runtime-config.ts +0 -14
  85. package/src/app/lib/registry-api/auth.ts +29 -46
  86. package/src/app/lib/registry-api/errors.ts +2 -2
  87. package/src/app/lib/registry-api/handler.ts +19 -14
  88. package/src/app/lib/registry-api/token-transfer.ts +1 -1
  89. package/src/app/lib/registry-api/transfer.ts +4 -4
  90. package/src/app/next.config.mjs +12 -3
  91. package/src/app/pages/_app.tsx +10 -4
  92. package/src/app/pages/account/index.tsx +153 -0
  93. package/src/app/pages/account/tokens.tsx +230 -0
  94. package/src/app/pages/account/users.tsx +310 -0
  95. package/src/app/pages/api/account/index.ts +26 -0
  96. package/src/app/pages/api/account/tokens/[id].ts +17 -0
  97. package/src/app/pages/api/account/tokens/index.ts +44 -0
  98. package/src/app/pages/api/admin/users/[id]/[action].ts +89 -0
  99. package/src/app/pages/api/admin/users/index.ts +53 -0
  100. package/src/app/pages/api/auth/[...nextauth].ts +4 -0
  101. package/src/app/pages/api/auth/request-reset.ts +53 -0
  102. package/src/app/pages/api/auth/reset-password.ts +29 -0
  103. package/src/app/pages/api/install.ts +102 -0
  104. package/src/app/pages/api/oauth/device/approve.ts +23 -0
  105. package/src/app/pages/api/oauth/device/index.ts +41 -0
  106. package/src/app/pages/api/oauth/revoke.ts +24 -0
  107. package/src/app/pages/api/oauth/token.ts +31 -0
  108. package/src/app/pages/api/registry/health.ts +4 -3
  109. package/src/app/pages/api/registry/transfer/[kind]/[id].ts +3 -3
  110. package/src/app/pages/api/registry/transfer/assets/[collection].ts +2 -1
  111. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +2 -1
  112. package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +2 -2
  113. package/src/app/pages/cli/device.tsx +143 -0
  114. package/src/app/pages/install.tsx +336 -0
  115. package/src/app/pages/login.tsx +117 -0
  116. package/src/app/pages/reset-password.tsx +159 -0
  117. package/src/app/types/next-auth.d.ts +27 -0
  118. package/src/app-builder/build.ts +22 -5
  119. package/src/app-builder/client-config.ts +10 -9
  120. package/src/cli/auth/browser.ts +29 -0
  121. package/src/cli/auth/device.ts +181 -0
  122. package/src/cli/auth/store.ts +137 -0
  123. package/src/cli/scaffold.ts +20 -197
  124. package/src/commands/index.ts +4 -0
  125. package/src/commands/login/index.ts +48 -0
  126. package/src/commands/logout/index.ts +51 -0
  127. package/src/config/entries.ts +175 -0
  128. package/src/config/index.ts +4 -0
  129. package/src/config/runtime.ts +15 -14
  130. package/src/registry/auth/access-tokens.ts +198 -0
  131. package/src/registry/auth/action-tokens.ts +225 -0
  132. package/src/registry/auth/crypto.ts +13 -0
  133. package/src/registry/auth/database.ts +15 -0
  134. package/src/registry/auth/device-authorizations.ts +223 -0
  135. package/src/registry/auth/gravatar.ts +6 -0
  136. package/src/registry/auth/index.ts +89 -0
  137. package/src/registry/auth/installation.ts +117 -0
  138. package/src/registry/auth/password.ts +46 -0
  139. package/src/registry/auth/rate-limits.ts +84 -0
  140. package/src/registry/auth/types.ts +34 -0
  141. package/src/registry/auth/user-record.ts +25 -0
  142. package/src/registry/auth/users.ts +145 -0
  143. package/src/registry/auth/validation.ts +24 -0
  144. package/src/registry/checkout/assets.ts +4 -2
  145. package/src/registry/checkout/index.ts +114 -24
  146. package/src/registry/checkout/tokens.ts +4 -2
  147. package/src/registry/connection.ts +23 -0
  148. package/src/registry/db/driver.ts +5 -4
  149. package/src/registry/db/index.ts +12 -0
  150. package/src/registry/db/schema.ts +174 -1
  151. package/src/registry/publish/assets.ts +1 -1
  152. package/src/registry/publish/errors.ts +3 -3
  153. package/src/registry/publish/index.ts +9 -9
  154. package/src/registry/publish/tokens.ts +1 -1
  155. package/src/types/config.ts +4 -1
  156. package/templates/default/README.md.tpl +18 -0
  157. package/templates/default/gitignore.tpl +1 -0
  158. package/templates/default/handoff.config.js.tpl +0 -1
  159. package/templates/default/handoff.config.ts.tpl +0 -1
  160. package/templates/default/handoff.config.with-examples.js.tpl +0 -1
  161. package/templates/default/handoff.config.with-examples.ts.tpl +0 -1
@@ -23,7 +23,7 @@
23
23
  * storage of large/binary assets (a content-reference field) rather than assuming inline content.
24
24
  */
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.registrySchema = exports.assetBlobs = exports.assets = exports.assetCollections = exports.tokenArtifacts = exports.tokenSets = exports.buildMetadata = exports.docsArtifacts = exports.pageFiles = exports.pages = exports.patternFiles = exports.patterns = exports.componentFiles = exports.components = void 0;
26
+ exports.registrySchema = exports.registryAuthRateLimits = exports.registryDeviceAuthorizations = exports.registryAccessTokens = exports.registryAuthActionTokens = exports.registryInstallations = exports.registryUsers = exports.assetBlobs = exports.assets = exports.assetCollections = exports.tokenArtifacts = exports.tokenSets = exports.buildMetadata = exports.docsArtifacts = exports.pageFiles = exports.pages = exports.patternFiles = exports.patterns = exports.componentFiles = exports.components = void 0;
27
27
  const drizzle_orm_1 = require("drizzle-orm");
28
28
  const pg_core_1 = require("drizzle-orm/pg-core");
29
29
  /**
@@ -329,6 +329,125 @@ exports.assetBlobs = (0, pg_core_1.pgTable)('asset_blobs', {
329
329
  size: (0, pg_core_1.integer)('size').notNull(),
330
330
  createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull().defaultNow(),
331
331
  });
332
+ /**
333
+ * Registry users. Browser sessions carry the id and auth version, while protected requests re-read
334
+ * this row so role/status changes take effect without waiting for a cookie to expire.
335
+ */
336
+ exports.registryUsers = (0, pg_core_1.pgTable)('registry_users', {
337
+ id: (0, pg_core_1.text)('id')
338
+ .primaryKey()
339
+ .$defaultFn(() => crypto.randomUUID()),
340
+ email: (0, pg_core_1.text)('email').notNull(),
341
+ name: (0, pg_core_1.text)('name'),
342
+ image: (0, pg_core_1.text)('image'),
343
+ role: (0, pg_core_1.text)('role').$type().notNull().default('member'),
344
+ status: (0, pg_core_1.text)('status').$type().notNull().default('invited'),
345
+ passwordHash: (0, pg_core_1.text)('password_hash'),
346
+ emailVerifiedAt: (0, pg_core_1.timestamp)('email_verified_at', { withTimezone: true }),
347
+ authVersion: (0, pg_core_1.integer)('auth_version').notNull().default(1),
348
+ createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull().defaultNow(),
349
+ updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }).notNull().defaultNow(),
350
+ }, (table) => [
351
+ (0, pg_core_1.uniqueIndex)('registry_users_normalized_email_idx').on((0, drizzle_orm_1.sql) `lower(btrim(${table.email}))`),
352
+ (0, pg_core_1.index)('registry_users_status_role_idx').on(table.status, table.role),
353
+ (0, pg_core_1.check)('registry_users_email_normalized', (0, drizzle_orm_1.sql) `${table.email} = lower(btrim(${table.email}))`),
354
+ (0, pg_core_1.check)('registry_users_role_valid', (0, drizzle_orm_1.sql) `${table.role} in ('admin', 'member')`),
355
+ (0, pg_core_1.check)('registry_users_status_valid', (0, drizzle_orm_1.sql) `${table.status} in ('invited', 'active', 'deactivated')`),
356
+ (0, pg_core_1.check)('registry_users_auth_version_positive', (0, drizzle_orm_1.sql) `${table.authVersion} > 0`),
357
+ ]);
358
+ /**
359
+ * Permanent one-row installation marker. There is deliberately no reset path: deleting or
360
+ * deactivating users must never reopen the public first-visitor installer.
361
+ */
362
+ exports.registryInstallations = (0, pg_core_1.pgTable)('registry_installations', {
363
+ id: (0, pg_core_1.text)('id').primaryKey().default('default'),
364
+ status: (0, pg_core_1.text)('status').notNull().default('installed'),
365
+ schemaVersion: (0, pg_core_1.integer)('schema_version').notNull(),
366
+ initialAdminUserId: (0, pg_core_1.text)('initial_admin_user_id')
367
+ .notNull()
368
+ .references(() => exports.registryUsers.id, { onDelete: 'restrict' }),
369
+ installedAt: (0, pg_core_1.timestamp)('installed_at', { withTimezone: true }).notNull().defaultNow(),
370
+ }, (table) => [
371
+ (0, pg_core_1.check)('registry_installations_singleton', (0, drizzle_orm_1.sql) `${table.id} = 'default'`),
372
+ (0, pg_core_1.check)('registry_installations_status_valid', (0, drizzle_orm_1.sql) `${table.status} = 'installed'`),
373
+ (0, pg_core_1.check)('registry_installations_schema_version_positive', (0, drizzle_orm_1.sql) `${table.schemaVersion} > 0`),
374
+ ]);
375
+ /** Hash-only invitation and password-reset secrets. */
376
+ exports.registryAuthActionTokens = (0, pg_core_1.pgTable)('registry_auth_action_tokens', {
377
+ id: (0, pg_core_1.text)('id')
378
+ .primaryKey()
379
+ .$defaultFn(() => crypto.randomUUID()),
380
+ userId: (0, pg_core_1.text)('user_id')
381
+ .notNull()
382
+ .references(() => exports.registryUsers.id, { onDelete: 'cascade' }),
383
+ purpose: (0, pg_core_1.text)('purpose').$type().notNull(),
384
+ tokenHash: (0, pg_core_1.text)('token_hash').notNull(),
385
+ expiresAt: (0, pg_core_1.timestamp)('expires_at', { withTimezone: true }).notNull(),
386
+ usedAt: (0, pg_core_1.timestamp)('used_at', { withTimezone: true }),
387
+ createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull().defaultNow(),
388
+ }, (table) => [
389
+ (0, pg_core_1.uniqueIndex)('registry_auth_action_tokens_hash_idx').on(table.tokenHash),
390
+ (0, pg_core_1.index)('registry_auth_action_tokens_user_purpose_idx').on(table.userId, table.purpose),
391
+ (0, pg_core_1.check)('registry_auth_action_tokens_purpose_valid', (0, drizzle_orm_1.sql) `${table.purpose} in ('invite', 'password_reset')`),
392
+ ]);
393
+ /** Revocable, hash-only registry API credentials owned by one user. */
394
+ exports.registryAccessTokens = (0, pg_core_1.pgTable)('registry_access_tokens', {
395
+ /** Public lookup id embedded in the opaque token; not itself a credential. */
396
+ id: (0, pg_core_1.text)('id').primaryKey(),
397
+ userId: (0, pg_core_1.text)('user_id')
398
+ .notNull()
399
+ .references(() => exports.registryUsers.id, { onDelete: 'cascade' }),
400
+ name: (0, pg_core_1.text)('name').notNull(),
401
+ secretHash: (0, pg_core_1.text)('secret_hash').notNull(),
402
+ scopes: (0, pg_core_1.jsonb)('scopes').$type().notNull(),
403
+ expiresAt: (0, pg_core_1.timestamp)('expires_at', { withTimezone: true }).notNull(),
404
+ lastUsedAt: (0, pg_core_1.timestamp)('last_used_at', { withTimezone: true }),
405
+ revokedAt: (0, pg_core_1.timestamp)('revoked_at', { withTimezone: true }),
406
+ createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull().defaultNow(),
407
+ }, (table) => [
408
+ (0, pg_core_1.index)('registry_access_tokens_user_idx').on(table.userId),
409
+ (0, pg_core_1.index)('registry_access_tokens_expires_idx').on(table.expiresAt),
410
+ (0, pg_core_1.check)('registry_access_tokens_name_not_blank', (0, drizzle_orm_1.sql) `length(btrim(${table.name})) > 0`),
411
+ (0, pg_core_1.check)('registry_access_tokens_scopes_array', (0, drizzle_orm_1.sql) `jsonb_typeof(${table.scopes}) = 'array'`),
412
+ ]);
413
+ /** Short-lived authorization used by `handoff-app login`. Plaintext device codes are never stored. */
414
+ exports.registryDeviceAuthorizations = (0, pg_core_1.pgTable)('registry_device_authorizations', {
415
+ id: (0, pg_core_1.text)('id')
416
+ .primaryKey()
417
+ .$defaultFn(() => crypto.randomUUID()),
418
+ deviceCodeHash: (0, pg_core_1.text)('device_code_hash').notNull(),
419
+ userCode: (0, pg_core_1.text)('user_code').notNull(),
420
+ status: (0, pg_core_1.text)('status').$type().notNull().default('pending'),
421
+ userId: (0, pg_core_1.text)('user_id').references(() => exports.registryUsers.id, { onDelete: 'set null' }),
422
+ scopes: (0, pg_core_1.jsonb)('scopes').$type().notNull(),
423
+ expiresAt: (0, pg_core_1.timestamp)('expires_at', { withTimezone: true }).notNull(),
424
+ approvedAt: (0, pg_core_1.timestamp)('approved_at', { withTimezone: true }),
425
+ consumedAt: (0, pg_core_1.timestamp)('consumed_at', { withTimezone: true }),
426
+ createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull().defaultNow(),
427
+ }, (table) => [
428
+ (0, pg_core_1.uniqueIndex)('registry_device_authorizations_device_hash_idx').on(table.deviceCodeHash),
429
+ (0, pg_core_1.uniqueIndex)('registry_device_authorizations_user_code_idx').on(table.userCode),
430
+ (0, pg_core_1.index)('registry_device_authorizations_expires_idx').on(table.expiresAt),
431
+ (0, pg_core_1.check)('registry_device_authorizations_status_valid', (0, drizzle_orm_1.sql) `${table.status} in ('pending', 'approved', 'denied', 'consumed')`),
432
+ (0, pg_core_1.check)('registry_device_authorizations_scopes_array', (0, drizzle_orm_1.sql) `jsonb_typeof(${table.scopes}) = 'array'`),
433
+ ]);
434
+ /**
435
+ * Fixed-window throttling counters. Callers pass a hash of the identifying value so raw emails and
436
+ * IP addresses are not retained.
437
+ */
438
+ exports.registryAuthRateLimits = (0, pg_core_1.pgTable)('registry_auth_rate_limits', {
439
+ bucket: (0, pg_core_1.text)('bucket').$type().notNull(),
440
+ identifierHash: (0, pg_core_1.text)('identifier_hash').notNull(),
441
+ windowStartedAt: (0, pg_core_1.timestamp)('window_started_at', { withTimezone: true }).notNull(),
442
+ attempts: (0, pg_core_1.integer)('attempts').notNull().default(1),
443
+ expiresAt: (0, pg_core_1.timestamp)('expires_at', { withTimezone: true }).notNull(),
444
+ updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }).notNull().defaultNow(),
445
+ }, (table) => [
446
+ (0, pg_core_1.primaryKey)({ columns: [table.bucket, table.identifierHash, table.windowStartedAt] }),
447
+ (0, pg_core_1.index)('registry_auth_rate_limits_expires_idx').on(table.expiresAt),
448
+ (0, pg_core_1.check)('registry_auth_rate_limits_bucket_valid', (0, drizzle_orm_1.sql) `${table.bucket} in ('login', 'password_reset', 'device')`),
449
+ (0, pg_core_1.check)('registry_auth_rate_limits_attempts_positive', (0, drizzle_orm_1.sql) `${table.attempts} > 0`),
450
+ ]);
332
451
  /** All registry tables, for typed Drizzle clients and migration tooling. */
333
452
  exports.registrySchema = {
334
453
  components: exports.components,
@@ -344,4 +463,10 @@ exports.registrySchema = {
344
463
  assetCollections: exports.assetCollections,
345
464
  assets: exports.assets,
346
465
  assetBlobs: exports.assetBlobs,
466
+ registryUsers: exports.registryUsers,
467
+ registryInstallations: exports.registryInstallations,
468
+ registryAuthActionTokens: exports.registryAuthActionTokens,
469
+ registryAccessTokens: exports.registryAccessTokens,
470
+ registryDeviceAuthorizations: exports.registryDeviceAuthorizations,
471
+ registryAuthRateLimits: exports.registryAuthRateLimits,
347
472
  };
@@ -50,7 +50,7 @@ const toManifestEntry = (asset) => {
50
50
  * throws at the end if any collection failed.
51
51
  */
52
52
  const publishAssets = (handoff, collection) => __awaiter(void 0, void 0, void 0, function* () {
53
- const connection = (0, index_1.resolveConnectionOrThrow)(handoff);
53
+ const connection = yield (0, index_1.resolveConnectionOrThrow)(handoff);
54
54
  if (collection && !(0, sets_1.isAssetCollection)(collection)) {
55
55
  throw new index_1.PublishError(`Unknown asset collection "${collection}". Supported collections: ${sets_1.ASSET_COLLECTIONS.join(', ')}.`);
56
56
  }
@@ -7,10 +7,10 @@ const describeUploadFailure = (error, registryUrl, rejectedPayload) => {
7
7
  switch (error.code) {
8
8
  case 'runtime_mode_conflict':
9
9
  return `The registry at ${registryUrl} is not running in registry mode, so it cannot accept publishes: ${error.message}`;
10
- case 'token_not_configured':
11
- return `The registry at ${registryUrl} has no management token configured, so it is rejecting mutations: ${error.message}`;
12
10
  case 'unauthorized':
13
- return `The registry rejected the access token (401). Check the configured access token matches the registry's token.`;
11
+ return `The registry rejected the access token (401). Run \`handoff-app login --url ${registryUrl}\` again, or replace the user-issued CI token.`;
12
+ case 'forbidden':
13
+ return `The registry token does not have permission to publish (403). Sign in as an administrator and authorize a token with registry:write access.`;
14
14
  case 'bad_request':
15
15
  return `The registry rejected the ${rejectedPayload} (400): ${error.message}`;
16
16
  default:
@@ -19,7 +19,7 @@ export declare class PublishError extends Error {
19
19
  * hosts do not publish) and a resolved registry URL + access token. Throws an actionable
20
20
  * {@link PublishError} naming the exact misconfiguration.
21
21
  */
22
- export declare const resolveConnectionOrThrow: (handoff: Handoff) => import("../connection").ResolvedRegistryConnection;
22
+ export declare const resolveConnectionOrThrow: (handoff: Handoff) => Promise<import("../connection").ResolvedRegistryConnection>;
23
23
  /**
24
24
  * Publish a single component, pattern, or page from a connected workspace. Components and patterns
25
25
  * receive a targeted build, while pages are packaged directly from their source.
@@ -45,24 +45,24 @@ exports.PublishError = PublishError;
45
45
  * hosts do not publish) and a resolved registry URL + access token. Throws an actionable
46
46
  * {@link PublishError} naming the exact misconfiguration.
47
47
  */
48
- const resolveConnectionOrThrow = (handoff) => {
48
+ const resolveConnectionOrThrow = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
49
49
  var _a, _b, _c;
50
50
  const mode = (_c = (_b = (_a = handoff.config) === null || _a === void 0 ? void 0 : _a.runtime) === null || _b === void 0 ? void 0 : _b.mode) !== null && _c !== void 0 ? _c : 'workspace';
51
51
  if (mode !== 'workspace') {
52
52
  throw new PublishError(`publish is only available from a connected workspace (runtime.mode: "workspace"); this project is "${mode}". ` +
53
53
  'A registry host stores and serves what was published to it; it does not publish.');
54
54
  }
55
- const connection = (0, connection_1.resolveRegistryConnection)(handoff.config);
55
+ const connection = yield (0, connection_1.resolveAuthenticatedRegistryConnection)(handoff.config, handoff.workingPath);
56
56
  if (!connection.url) {
57
- throw new PublishError(`No registry URL is configured. Set runtime.registryConnection.url, or the "${connection.urlEnv}" environment variable, ` +
58
- 'to the base URL of the registry to publish to.');
57
+ throw new PublishError(`No registry is configured. Run \`handoff-app login --url <registry-url>\`, set runtime.registryConnection.url, ` +
58
+ `or set the "${connection.urlEnv}" environment variable to the base URL of the registry to publish to.`);
59
59
  }
60
60
  if (!connection.accessToken) {
61
- throw new PublishError(`No registry access token is configured. Set the "${connection.accessTokenEnv}" environment variable to the ` +
62
- "registry's bearer token to authorize publishing.");
61
+ throw new PublishError(`No registry access token is configured. Run \`handoff-app login --url ${connection.url}\`, or set the ` +
62
+ `"${connection.accessTokenEnv}" environment variable to a user-issued token for CI.`);
63
63
  }
64
64
  return connection;
65
- };
65
+ });
66
66
  exports.resolveConnectionOrThrow = resolveConnectionOrThrow;
67
67
  /** Run the smallest build that guarantees the selected entity's artifacts are current. */
68
68
  const runTargetedBuild = (handoff, kind, id) => __awaiter(void 0, void 0, void 0, function* () {
@@ -105,7 +105,7 @@ const runTargetedBuild = (handoff, kind, id) => __awaiter(void 0, void 0, void 0
105
105
  * receive a targeted build, while pages are packaged directly from their source.
106
106
  */
107
107
  const publishEntity = (handoff, kind, id) => __awaiter(void 0, void 0, void 0, function* () {
108
- const connection = (0, exports.resolveConnectionOrThrow)(handoff);
108
+ const connection = yield (0, exports.resolveConnectionOrThrow)(handoff);
109
109
  logger_1.Logger.info(kind === 'page' ? `Preparing page "${id}" for publish…` : `Building ${kind} "${id}" for publish…`);
110
110
  yield runTargetedBuild(handoff, kind, id);
111
111
  const pkg = yield (0, package_1.buildPublishPackage)(handoff, kind, id);
@@ -154,7 +154,7 @@ const listWorkspaceEntityIds = (handoff, kind) => __awaiter(void 0, void 0, void
154
154
  * collected and never aborts the rest; the run throws at the end if any entity failed.
155
155
  */
156
156
  const publishEntities = (handoff, kind) => __awaiter(void 0, void 0, void 0, function* () {
157
- const connection = (0, exports.resolveConnectionOrThrow)(handoff);
157
+ const connection = yield (0, exports.resolveConnectionOrThrow)(handoff);
158
158
  logger_1.Logger.info(`Building ${kind}s for publish…`);
159
159
  yield runBulkBuild(handoff, kind);
160
160
  const ids = yield listWorkspaceEntityIds(handoff, kind);
@@ -59,7 +59,7 @@ const buildTokenSetPackage = (handoff, set) => __awaiter(void 0, void 0, void 0,
59
59
  * committed atomically server-side); the run throws at the end if any set failed.
60
60
  */
61
61
  const publishTokens = (handoff, setId) => __awaiter(void 0, void 0, void 0, function* () {
62
- const connection = (0, index_1.resolveConnectionOrThrow)(handoff);
62
+ const connection = yield (0, index_1.resolveConnectionOrThrow)(handoff);
63
63
  logger_1.Logger.info(setId ? `Building tokens to publish "${setId}"…` : 'Building tokens for publish…');
64
64
  yield handoff.fetch();
65
65
  const allSets = yield handoff.store.tokens.listSets();
@@ -136,7 +136,10 @@ export interface HandoffRuntimeConfig {
136
136
  registry?: {
137
137
  /** Name of the env var holding the database URL. @default "DATABASE_URL" */
138
138
  databaseUrlEnv?: string;
139
- /** Name of the env var holding the registry API token. @default "HANDOFF_REGISTRY_API_TOKEN" */
139
+ /**
140
+ * @deprecated Fixed registry secrets are no longer authorized. This option remains parseable
141
+ * only so existing configuration files do not break.
142
+ */
140
143
  apiTokenEnv?: string;
141
144
  database?: {
142
145
  /**
package/docs/cli.md CHANGED
@@ -58,6 +58,10 @@ Commands:
58
58
 
59
59
  db:migrate [opts] - Runs registry database migrations (Drizzle / PostgreSQL). Reads the project config and database env vars, applies the package-owned migration set, and runs independently of `build`.
60
60
 
61
+ login --url <registry-url> - Authorizes this workspace in the browser and stores a revocable token in `.handoff/cli-auth.json`.
62
+
63
+ logout [--url <registry-url>] - Revokes the saved token when possible, then removes the local credential.
64
+
61
65
  make
62
66
  make:template <component> <state> [opts] - Creates a new template
63
67
  make:page <component> <state> [opts] - Creates a new page
@@ -0,0 +1,95 @@
1
+ CREATE TABLE "registry_access_tokens" (
2
+ "id" text PRIMARY KEY NOT NULL,
3
+ "user_id" text NOT NULL,
4
+ "name" text NOT NULL,
5
+ "secret_hash" text NOT NULL,
6
+ "scopes" jsonb NOT NULL,
7
+ "expires_at" timestamp with time zone NOT NULL,
8
+ "last_used_at" timestamp with time zone,
9
+ "revoked_at" timestamp with time zone,
10
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
11
+ CONSTRAINT "registry_access_tokens_name_not_blank" CHECK (length(btrim("registry_access_tokens"."name")) > 0),
12
+ CONSTRAINT "registry_access_tokens_scopes_array" CHECK (jsonb_typeof("registry_access_tokens"."scopes") = 'array')
13
+ );
14
+ --> statement-breakpoint
15
+ CREATE TABLE "registry_auth_action_tokens" (
16
+ "id" text PRIMARY KEY NOT NULL,
17
+ "user_id" text NOT NULL,
18
+ "purpose" text NOT NULL,
19
+ "token_hash" text NOT NULL,
20
+ "expires_at" timestamp with time zone NOT NULL,
21
+ "used_at" timestamp with time zone,
22
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
23
+ CONSTRAINT "registry_auth_action_tokens_purpose_valid" CHECK ("registry_auth_action_tokens"."purpose" in ('invite', 'password_reset'))
24
+ );
25
+ --> statement-breakpoint
26
+ CREATE TABLE "registry_auth_rate_limits" (
27
+ "bucket" text NOT NULL,
28
+ "identifier_hash" text NOT NULL,
29
+ "window_started_at" timestamp with time zone NOT NULL,
30
+ "attempts" integer DEFAULT 1 NOT NULL,
31
+ "expires_at" timestamp with time zone NOT NULL,
32
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
33
+ CONSTRAINT "registry_auth_rate_limits_bucket_identifier_hash_window_started_at_pk" PRIMARY KEY("bucket","identifier_hash","window_started_at"),
34
+ CONSTRAINT "registry_auth_rate_limits_bucket_valid" CHECK ("registry_auth_rate_limits"."bucket" in ('login', 'password_reset', 'device')),
35
+ CONSTRAINT "registry_auth_rate_limits_attempts_positive" CHECK ("registry_auth_rate_limits"."attempts" > 0)
36
+ );
37
+ --> statement-breakpoint
38
+ CREATE TABLE "registry_device_authorizations" (
39
+ "id" text PRIMARY KEY NOT NULL,
40
+ "device_code_hash" text NOT NULL,
41
+ "user_code" text NOT NULL,
42
+ "status" text DEFAULT 'pending' NOT NULL,
43
+ "user_id" text,
44
+ "scopes" jsonb NOT NULL,
45
+ "expires_at" timestamp with time zone NOT NULL,
46
+ "approved_at" timestamp with time zone,
47
+ "consumed_at" timestamp with time zone,
48
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
49
+ CONSTRAINT "registry_device_authorizations_status_valid" CHECK ("registry_device_authorizations"."status" in ('pending', 'approved', 'denied', 'consumed')),
50
+ CONSTRAINT "registry_device_authorizations_scopes_array" CHECK (jsonb_typeof("registry_device_authorizations"."scopes") = 'array')
51
+ );
52
+ --> statement-breakpoint
53
+ CREATE TABLE "registry_installations" (
54
+ "id" text PRIMARY KEY DEFAULT 'default' NOT NULL,
55
+ "status" text DEFAULT 'installed' NOT NULL,
56
+ "schema_version" integer NOT NULL,
57
+ "initial_admin_user_id" text NOT NULL,
58
+ "installed_at" timestamp with time zone DEFAULT now() NOT NULL,
59
+ CONSTRAINT "registry_installations_singleton" CHECK ("registry_installations"."id" = 'default'),
60
+ CONSTRAINT "registry_installations_status_valid" CHECK ("registry_installations"."status" = 'installed'),
61
+ CONSTRAINT "registry_installations_schema_version_positive" CHECK ("registry_installations"."schema_version" > 0)
62
+ );
63
+ --> statement-breakpoint
64
+ CREATE TABLE "registry_users" (
65
+ "id" text PRIMARY KEY NOT NULL,
66
+ "email" text NOT NULL,
67
+ "name" text,
68
+ "image" text,
69
+ "role" text DEFAULT 'member' NOT NULL,
70
+ "status" text DEFAULT 'invited' NOT NULL,
71
+ "password_hash" text,
72
+ "email_verified_at" timestamp with time zone,
73
+ "auth_version" integer DEFAULT 1 NOT NULL,
74
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
75
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
76
+ CONSTRAINT "registry_users_email_normalized" CHECK ("registry_users"."email" = lower(btrim("registry_users"."email"))),
77
+ CONSTRAINT "registry_users_role_valid" CHECK ("registry_users"."role" in ('admin', 'member')),
78
+ CONSTRAINT "registry_users_status_valid" CHECK ("registry_users"."status" in ('invited', 'active', 'deactivated')),
79
+ CONSTRAINT "registry_users_auth_version_positive" CHECK ("registry_users"."auth_version" > 0)
80
+ );
81
+ --> statement-breakpoint
82
+ ALTER TABLE "registry_access_tokens" ADD CONSTRAINT "registry_access_tokens_user_id_registry_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."registry_users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
83
+ ALTER TABLE "registry_auth_action_tokens" ADD CONSTRAINT "registry_auth_action_tokens_user_id_registry_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."registry_users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
84
+ ALTER TABLE "registry_device_authorizations" ADD CONSTRAINT "registry_device_authorizations_user_id_registry_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."registry_users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
85
+ ALTER TABLE "registry_installations" ADD CONSTRAINT "registry_installations_initial_admin_user_id_registry_users_id_fk" FOREIGN KEY ("initial_admin_user_id") REFERENCES "public"."registry_users"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
86
+ CREATE INDEX "registry_access_tokens_user_idx" ON "registry_access_tokens" USING btree ("user_id");--> statement-breakpoint
87
+ CREATE INDEX "registry_access_tokens_expires_idx" ON "registry_access_tokens" USING btree ("expires_at");--> statement-breakpoint
88
+ CREATE UNIQUE INDEX "registry_auth_action_tokens_hash_idx" ON "registry_auth_action_tokens" USING btree ("token_hash");--> statement-breakpoint
89
+ CREATE INDEX "registry_auth_action_tokens_user_purpose_idx" ON "registry_auth_action_tokens" USING btree ("user_id","purpose");--> statement-breakpoint
90
+ CREATE INDEX "registry_auth_rate_limits_expires_idx" ON "registry_auth_rate_limits" USING btree ("expires_at");--> statement-breakpoint
91
+ CREATE UNIQUE INDEX "registry_device_authorizations_device_hash_idx" ON "registry_device_authorizations" USING btree ("device_code_hash");--> statement-breakpoint
92
+ CREATE UNIQUE INDEX "registry_device_authorizations_user_code_idx" ON "registry_device_authorizations" USING btree ("user_code");--> statement-breakpoint
93
+ CREATE INDEX "registry_device_authorizations_expires_idx" ON "registry_device_authorizations" USING btree ("expires_at");--> statement-breakpoint
94
+ CREATE UNIQUE INDEX "registry_users_normalized_email_idx" ON "registry_users" USING btree (lower(btrim("email")));--> statement-breakpoint
95
+ CREATE INDEX "registry_users_status_role_idx" ON "registry_users" USING btree ("status","role");