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.
- package/README.md +140 -83
- package/dist/app-builder/build.js +21 -4
- package/dist/app-builder/client-config.js +9 -8
- package/dist/cli/auth/browser.d.ts +2 -0
- package/dist/cli/auth/browser.js +33 -0
- package/dist/cli/auth/device.d.ts +11 -0
- package/dist/cli/auth/device.js +155 -0
- package/dist/cli/auth/store.d.ts +29 -0
- package/dist/cli/auth/store.js +146 -0
- package/dist/cli/scaffold.js +20 -154
- package/dist/commands/index.js +4 -0
- package/dist/commands/login/index.d.ts +8 -0
- package/dist/commands/login/index.js +53 -0
- package/dist/commands/logout/index.d.ts +7 -0
- package/dist/commands/logout/index.js +54 -0
- package/dist/config/entries.d.ts +47 -0
- package/dist/config/entries.js +162 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +5 -1
- package/dist/config/runtime.d.ts +9 -9
- package/dist/config/runtime.js +15 -15
- package/dist/registry/auth/access-tokens.d.ts +46 -0
- package/dist/registry/auth/access-tokens.js +165 -0
- package/dist/registry/auth/action-tokens.d.ts +66 -0
- package/dist/registry/auth/action-tokens.js +178 -0
- package/dist/registry/auth/crypto.d.ts +4 -0
- package/dist/registry/auth/crypto.js +16 -0
- package/dist/registry/auth/database.d.ts +7 -0
- package/dist/registry/auth/database.js +22 -0
- package/dist/registry/auth/device-authorizations.d.ts +51 -0
- package/dist/registry/auth/device-authorizations.js +170 -0
- package/dist/registry/auth/gravatar.d.ts +1 -0
- package/dist/registry/auth/gravatar.js +8 -0
- package/dist/registry/auth/index.d.ts +11 -0
- package/dist/registry/auth/index.js +72 -0
- package/dist/registry/auth/installation.d.ts +42 -0
- package/dist/registry/auth/installation.js +105 -0
- package/dist/registry/auth/password.d.ts +12 -0
- package/dist/registry/auth/password.js +58 -0
- package/dist/registry/auth/rate-limits.d.ts +22 -0
- package/dist/registry/auth/rate-limits.js +75 -0
- package/dist/registry/auth/types.d.ts +26 -0
- package/dist/registry/auth/types.js +9 -0
- package/dist/registry/auth/user-record.d.ts +13 -0
- package/dist/registry/auth/user-record.js +16 -0
- package/dist/registry/auth/users.d.ts +31 -0
- package/dist/registry/auth/users.js +135 -0
- package/dist/registry/auth/validation.d.ts +3 -0
- package/dist/registry/auth/validation.js +28 -0
- package/dist/registry/checkout/assets.js +4 -2
- package/dist/registry/checkout/index.d.ts +1 -1
- package/dist/registry/checkout/index.js +101 -23
- package/dist/registry/checkout/tokens.js +4 -2
- package/dist/registry/connection.d.ts +9 -0
- package/dist/registry/connection.js +29 -1
- package/dist/registry/db/driver.d.ts +5 -4
- package/dist/registry/db/driver.js +5 -4
- package/dist/registry/db/index.d.ts +1 -1
- package/dist/registry/db/index.js +7 -1
- package/dist/registry/db/schema.d.ts +2521 -750
- package/dist/registry/db/schema.js +126 -1
- package/dist/registry/publish/assets.js +1 -1
- package/dist/registry/publish/errors.js +3 -3
- package/dist/registry/publish/index.d.ts +1 -1
- package/dist/registry/publish/index.js +9 -9
- package/dist/registry/publish/tokens.js +1 -1
- package/dist/types/config.d.ts +4 -1
- package/docs/cli.md +4 -0
- package/drizzle/0005_add_registry_auth.sql +95 -0
- package/drizzle/meta/0005_snapshot.json +1926 -0
- package/drizzle/meta/_journal.json +8 -1
- package/package.json +5 -4
- package/src/app/components/Auth/AccountLayout.tsx +87 -0
- package/src/app/components/Auth/AuthControls.tsx +99 -0
- package/src/app/components/Auth/AuthShell.tsx +73 -0
- package/src/app/components/Auth/RegistryInstallGate.tsx +23 -0
- package/src/app/components/Auth/RegistrySessionProvider.tsx +15 -0
- package/src/app/components/Auth/api.ts +20 -0
- package/src/app/components/Layout/Header.tsx +2 -0
- package/src/app/components/Navigation/MobileNav.tsx +2 -0
- package/src/app/lib/auth/api.ts +113 -0
- package/src/app/lib/auth/config.ts +175 -0
- package/src/app/lib/auth/email.ts +34 -0
- package/src/app/lib/docs-api/runtime-config.ts +0 -14
- package/src/app/lib/registry-api/auth.ts +29 -46
- package/src/app/lib/registry-api/errors.ts +2 -2
- package/src/app/lib/registry-api/handler.ts +19 -14
- package/src/app/lib/registry-api/token-transfer.ts +1 -1
- package/src/app/lib/registry-api/transfer.ts +4 -4
- package/src/app/next.config.mjs +12 -3
- package/src/app/pages/_app.tsx +10 -4
- package/src/app/pages/account/index.tsx +153 -0
- package/src/app/pages/account/tokens.tsx +230 -0
- package/src/app/pages/account/users.tsx +310 -0
- package/src/app/pages/api/account/index.ts +26 -0
- package/src/app/pages/api/account/tokens/[id].ts +17 -0
- package/src/app/pages/api/account/tokens/index.ts +44 -0
- package/src/app/pages/api/admin/users/[id]/[action].ts +89 -0
- package/src/app/pages/api/admin/users/index.ts +53 -0
- package/src/app/pages/api/auth/[...nextauth].ts +4 -0
- package/src/app/pages/api/auth/request-reset.ts +53 -0
- package/src/app/pages/api/auth/reset-password.ts +29 -0
- package/src/app/pages/api/install.ts +102 -0
- package/src/app/pages/api/oauth/device/approve.ts +23 -0
- package/src/app/pages/api/oauth/device/index.ts +41 -0
- package/src/app/pages/api/oauth/revoke.ts +24 -0
- package/src/app/pages/api/oauth/token.ts +31 -0
- package/src/app/pages/api/registry/health.ts +4 -3
- package/src/app/pages/api/registry/transfer/[kind]/[id].ts +3 -3
- package/src/app/pages/api/registry/transfer/assets/[collection].ts +2 -1
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +2 -1
- package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +2 -2
- package/src/app/pages/cli/device.tsx +143 -0
- package/src/app/pages/install.tsx +336 -0
- package/src/app/pages/login.tsx +117 -0
- package/src/app/pages/reset-password.tsx +159 -0
- package/src/app/types/next-auth.d.ts +27 -0
- package/src/app-builder/build.ts +22 -5
- package/src/app-builder/client-config.ts +10 -9
- package/src/cli/auth/browser.ts +29 -0
- package/src/cli/auth/device.ts +181 -0
- package/src/cli/auth/store.ts +137 -0
- package/src/cli/scaffold.ts +20 -197
- package/src/commands/index.ts +4 -0
- package/src/commands/login/index.ts +48 -0
- package/src/commands/logout/index.ts +51 -0
- package/src/config/entries.ts +175 -0
- package/src/config/index.ts +4 -0
- package/src/config/runtime.ts +15 -14
- package/src/registry/auth/access-tokens.ts +198 -0
- package/src/registry/auth/action-tokens.ts +225 -0
- package/src/registry/auth/crypto.ts +13 -0
- package/src/registry/auth/database.ts +15 -0
- package/src/registry/auth/device-authorizations.ts +223 -0
- package/src/registry/auth/gravatar.ts +6 -0
- package/src/registry/auth/index.ts +89 -0
- package/src/registry/auth/installation.ts +117 -0
- package/src/registry/auth/password.ts +46 -0
- package/src/registry/auth/rate-limits.ts +84 -0
- package/src/registry/auth/types.ts +34 -0
- package/src/registry/auth/user-record.ts +25 -0
- package/src/registry/auth/users.ts +145 -0
- package/src/registry/auth/validation.ts +24 -0
- package/src/registry/checkout/assets.ts +4 -2
- package/src/registry/checkout/index.ts +114 -24
- package/src/registry/checkout/tokens.ts +4 -2
- package/src/registry/connection.ts +23 -0
- package/src/registry/db/driver.ts +5 -4
- package/src/registry/db/index.ts +12 -0
- package/src/registry/db/schema.ts +174 -1
- package/src/registry/publish/assets.ts +1 -1
- package/src/registry/publish/errors.ts +3 -3
- package/src/registry/publish/index.ts +9 -9
- package/src/registry/publish/tokens.ts +1 -1
- package/src/types/config.ts +4 -1
- package/templates/default/README.md.tpl +18 -0
- package/templates/default/gitignore.tpl +1 -0
- package/templates/default/handoff.config.js.tpl +0 -1
- package/templates/default/handoff.config.ts.tpl +0 -1
- package/templates/default/handoff.config.with-examples.js.tpl +0 -1
- 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).
|
|
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.
|
|
55
|
+
const connection = yield (0, connection_1.resolveAuthenticatedRegistryConnection)(handoff.config, handoff.workingPath);
|
|
56
56
|
if (!connection.url) {
|
|
57
|
-
throw new PublishError(`No registry
|
|
58
|
-
|
|
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.
|
|
62
|
-
"
|
|
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();
|
package/dist/types/config.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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");
|