handoff-app 2.0.0-4 → 2.0.0-6
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/AGENTS.md +7 -0
- package/CLAUDE.md +7 -0
- package/README.md +346 -377
- package/dist/app-builder/build.js +11 -4
- package/dist/app-builder/client-config.js +1 -0
- package/dist/app-builder/nav-shell.js +8 -15
- package/dist/app-builder/vercel-output.js +4 -4
- package/dist/cli/make.js +2 -2
- package/dist/cli.js +38 -6
- package/dist/commands/build/app.js +1 -5
- package/dist/commands/build/components.js +1 -5
- package/dist/commands/build/index.js +1 -5
- package/dist/commands/checkout/index.d.ts +15 -12
- package/dist/commands/checkout/index.js +15 -54
- package/dist/commands/compat/deprecations.d.ts +46 -0
- package/dist/commands/compat/deprecations.js +62 -0
- package/dist/commands/compat/push-args.d.ts +48 -0
- package/dist/commands/compat/push-args.js +73 -0
- package/dist/commands/db/migrate.js +1 -5
- package/dist/commands/dev/index.js +1 -5
- package/dist/commands/eject/config.js +1 -5
- package/dist/commands/eject/pages.js +1 -5
- package/dist/commands/eject/theme.js +1 -5
- package/dist/commands/fetch/index.js +1 -5
- package/dist/commands/index.js +4 -0
- package/dist/commands/login/index.js +1 -5
- package/dist/commands/logout/index.js +1 -5
- package/dist/commands/make/component.js +1 -5
- package/dist/commands/make/page.js +3 -7
- package/dist/commands/make/template.js +1 -5
- package/dist/commands/publish/index.d.ts +18 -12
- package/dist/commands/publish/index.js +18 -54
- package/dist/commands/pull/index.d.ts +13 -0
- package/dist/commands/pull/index.js +20 -0
- package/dist/commands/push/index.d.ts +23 -0
- package/dist/commands/push/index.js +58 -0
- package/dist/commands/scaffold/index.js +1 -5
- package/dist/commands/start/index.js +1 -5
- package/dist/commands/types.d.ts +10 -0
- package/dist/commands/utils.d.ts +74 -6
- package/dist/commands/utils.js +89 -10
- package/dist/commands/validate/components.js +1 -5
- package/dist/config/defaults.d.ts +2 -0
- package/dist/config/defaults.js +4 -1
- package/dist/config/index.d.ts +3 -2
- package/dist/config/index.js +3 -1
- package/dist/config/loader.d.ts +19 -6
- package/dist/config/loader.js +43 -13
- package/dist/config/normalizers/page.d.ts +4 -4
- package/dist/config/normalizers/page.js +2 -2
- package/dist/config/runtime.js +9 -25
- package/dist/index.d.ts +69 -22
- package/dist/index.js +148 -35
- package/dist/nav/resolver.js +3 -1
- package/dist/nav/workspace.js +2 -2
- package/dist/registry/auth/access-tokens.d.ts +11 -3
- package/dist/registry/auth/access-tokens.js +29 -3
- package/dist/registry/auth/index.d.ts +4 -3
- package/dist/registry/auth/index.js +10 -2
- package/dist/registry/auth/sync-secret.d.ts +27 -0
- package/dist/registry/auth/sync-secret.js +65 -0
- package/dist/registry/auth/types.d.ts +20 -1
- package/dist/registry/auth/types.js +3 -1
- package/dist/registry/auth/validation.d.ts +11 -0
- package/dist/registry/auth/validation.js +18 -1
- package/dist/registry/checkout/assets.d.ts +1 -1
- package/dist/registry/checkout/assets.js +21 -6
- package/dist/registry/checkout/index.d.ts +8 -1
- package/dist/registry/checkout/index.js +39 -6
- package/dist/registry/checkout/tokens.d.ts +1 -1
- package/dist/registry/checkout/tokens.js +12 -5
- package/dist/registry/connection.d.ts +10 -0
- package/dist/registry/connection.js +52 -7
- package/dist/registry/content-kinds.d.ts +27 -0
- package/dist/registry/content-kinds.js +33 -0
- package/dist/registry/db/client.d.ts +6 -1
- package/dist/registry/db/client.js +16 -3
- package/dist/registry/db/schema.d.ts +37 -3
- package/dist/registry/db/schema.js +9 -7
- package/dist/registry/publish/assets.d.ts +2 -2
- package/dist/registry/publish/assets.js +46 -35
- package/dist/registry/publish/index.d.ts +29 -5
- package/dist/registry/publish/index.js +87 -34
- package/dist/registry/publish/tokens.d.ts +2 -2
- package/dist/registry/publish/tokens.js +30 -24
- package/dist/registry/selection.d.ts +12 -0
- package/dist/registry/selection.js +22 -0
- package/dist/store/registry.d.ts +48 -0
- package/dist/store/registry.js +75 -12
- package/dist/transformers/preview/types.d.ts +2 -2
- package/dist/types/config.d.ts +7 -0
- package/dist/utils/markdown.d.ts +10 -0
- package/dist/utils/markdown.js +17 -0
- package/dist/utils/menu-shell.js +14 -7
- package/dist/utils/pages.d.ts +22 -0
- package/dist/utils/pages.js +73 -0
- package/dist/utils/path.d.ts +5 -0
- package/dist/utils/path.js +9 -0
- package/docs/api.md +1 -3
- package/docs/cli.md +1 -1
- package/drizzle/0002_add_pages.sql +5 -3
- package/drizzle/meta/0002_snapshot.json +17 -8
- package/drizzle/meta/0003_snapshot.json +17 -8
- package/drizzle/meta/0004_snapshot.json +17 -8
- package/drizzle/meta/0005_snapshot.json +17 -8
- package/package.json +4 -2
- package/src/app/components/CopyToClipboard.tsx +27 -26
- package/src/app/components/Layout/Header.tsx +4 -12
- package/src/app/components/Layout/RuntimeModeBadge.tsx +18 -0
- package/src/app/components/McpIntegration/McpConfigDialog.tsx +349 -0
- package/src/app/components/McpIntegration/McpIcon.tsx +10 -0
- package/src/app/components/Navigation/MobileNav.tsx +3 -1
- package/src/app/components/ui/dialog.tsx +73 -0
- package/src/app/components/util/index.ts +5 -23
- package/src/app/lib/docs-api/backend.ts +16 -1
- package/src/app/lib/docs-api/errors.ts +8 -7
- package/src/app/lib/docs-api/handler.ts +2 -3
- package/src/app/lib/docs-api/index.ts +2 -0
- package/src/app/lib/docs-api/page-discovery.ts +121 -0
- package/src/app/lib/docs-api/page-rendering.ts +37 -0
- package/src/app/lib/docs-api/page-search.ts +191 -0
- package/src/app/lib/docs-api/records.ts +31 -24
- package/src/app/lib/docs-api/registry.ts +92 -11
- package/src/app/lib/docs-api/runtime-config.ts +5 -0
- package/src/app/lib/docs-api/search.ts +120 -0
- package/src/app/lib/mcp/auth.ts +64 -0
- package/src/app/lib/mcp/index.ts +2 -0
- package/src/app/lib/mcp/server.ts +343 -0
- package/src/app/lib/mcp/shape.ts +595 -0
- package/src/app/lib/registry-api/allowlist.ts +7 -5
- package/src/app/lib/registry-api/asset-transfer.ts +58 -31
- package/src/app/lib/registry-api/auth.ts +89 -9
- package/src/app/lib/registry-api/entity-routes.ts +17 -7
- package/src/app/lib/registry-api/errors.ts +10 -4
- package/src/app/lib/registry-api/handler.ts +13 -15
- package/src/app/lib/registry-api/meta.ts +4 -0
- package/src/app/lib/registry-api/revalidate.ts +31 -12
- package/src/app/lib/registry-api/store.ts +89 -26
- package/src/app/lib/registry-api/sync-compat.ts +166 -0
- package/src/app/lib/registry-api/token-transfer.ts +44 -12
- package/src/app/lib/registry-api/transfer-batch.ts +116 -0
- package/src/app/lib/registry-api/transfer.ts +62 -35
- package/src/app/lib/registry-api/validation.ts +33 -1
- package/src/app/lib/registry-connection.ts +1 -1
- package/src/app/next.config.mjs +9 -0
- package/src/app/pages/[...slug]/index.tsx +52 -51
- package/src/app/pages/api/account/tokens/index.ts +4 -13
- package/src/app/pages/api/docs/search/pages.json.ts +25 -0
- package/src/app/pages/api/mcp.ts +56 -0
- package/src/app/pages/api/oauth/revoke.ts +14 -7
- package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +1 -1
- package/src/app/pages/api/registry/components/[id].ts +2 -2
- package/src/app/pages/api/registry/components/index.ts +1 -1
- package/src/app/pages/api/registry/pages/[...id].ts +20 -0
- package/src/app/pages/api/registry/pages/index.ts +19 -0
- package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +1 -1
- package/src/app/pages/api/registry/patterns/[id].ts +2 -2
- package/src/app/pages/api/registry/patterns/index.ts +1 -1
- package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +2 -2
- package/src/app/pages/api/sync/upload.ts +21 -0
- package/src/app/pages/index/index.tsx +14 -0
- package/src/app/pages/index.tsx +31 -5
- package/src/app-builder/build.ts +11 -4
- package/src/app-builder/client-config.ts +2 -1
- package/src/app-builder/nav-shell.ts +8 -13
- package/src/app-builder/vercel-output.ts +4 -4
- package/src/cli/make.ts +2 -2
- package/src/cli.ts +30 -6
- package/src/commands/build/app.ts +2 -3
- package/src/commands/build/components.ts +2 -3
- package/src/commands/build/index.ts +2 -3
- package/src/commands/checkout/index.ts +18 -44
- package/src/commands/compat/deprecations.ts +79 -0
- package/src/commands/compat/push-args.ts +106 -0
- package/src/commands/db/migrate.ts +2 -3
- package/src/commands/dev/index.ts +2 -3
- package/src/commands/eject/config.ts +2 -3
- package/src/commands/eject/pages.ts +2 -3
- package/src/commands/eject/theme.ts +2 -3
- package/src/commands/fetch/index.ts +2 -3
- package/src/commands/index.ts +4 -0
- package/src/commands/login/index.ts +2 -3
- package/src/commands/logout/index.ts +2 -3
- package/src/commands/make/component.ts +2 -3
- package/src/commands/make/page.ts +4 -5
- package/src/commands/make/template.ts +2 -3
- package/src/commands/publish/index.ts +21 -44
- package/src/commands/pull/index.ts +32 -0
- package/src/commands/push/index.ts +115 -0
- package/src/commands/scaffold/index.ts +2 -3
- package/src/commands/start/index.ts +2 -3
- package/src/commands/types.ts +11 -1
- package/src/commands/utils.ts +80 -11
- package/src/commands/validate/components.ts +2 -3
- package/src/config/defaults.ts +3 -0
- package/src/config/index.ts +3 -2
- package/src/config/loader.ts +57 -12
- package/src/config/normalizers/page.ts +4 -4
- package/src/config/runtime.ts +9 -24
- package/src/index.ts +183 -42
- package/src/nav/resolver.ts +3 -1
- package/src/nav/workspace.ts +2 -2
- package/src/registry/auth/access-tokens.ts +29 -5
- package/src/registry/auth/index.ts +7 -1
- package/src/registry/auth/sync-secret.ts +67 -0
- package/src/registry/auth/types.ts +24 -1
- package/src/registry/auth/validation.ts +18 -0
- package/src/registry/checkout/assets.ts +24 -7
- package/src/registry/checkout/index.ts +41 -5
- package/src/registry/checkout/tokens.ts +12 -6
- package/src/registry/connection.ts +53 -5
- package/src/registry/content-kinds.ts +39 -0
- package/src/registry/db/client.ts +22 -2
- package/src/registry/db/schema.ts +9 -7
- package/src/registry/publish/assets.ts +47 -37
- package/src/registry/publish/index.ts +102 -38
- package/src/registry/publish/tokens.ts +35 -26
- package/src/registry/selection.ts +19 -0
- package/src/store/registry.ts +118 -13
- package/src/transformers/preview/types.ts +2 -2
- package/src/types/config.ts +7 -0
- package/src/utils/README.md +1 -0
- package/src/utils/markdown.ts +11 -0
- package/src/utils/menu-shell.ts +14 -7
- package/src/utils/pages.ts +52 -0
- package/src/utils/path.ts +15 -6
- package/templates/default/README.md.tpl +10 -0
- package/templates/default/handoff.config.js.tpl +6 -3
- package/templates/default/handoff.config.ts.tpl +6 -3
- package/templates/default/handoff.config.with-examples.js.tpl +6 -3
- package/templates/default/handoff.config.with-examples.ts.tpl +6 -3
- package/templates/default/package.json.tpl +4 -0
- package/config/docs/foundations/logos.md +0 -7
- package/src/app/lib/api/redact.ts +0 -46
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.validateRegistryImageUrl = exports.validateRegistryEmail = exports.validateRegistryDisplayName = exports.registryPrincipalHasScope = exports.scopesForRegistryRole = exports.REGISTRY_WRITE_SCOPE = exports.REGISTRY_READ_SCOPE = void 0;
|
|
3
|
+
exports.getRegistryUserById = exports.hashRateLimitIdentifier = exports.purgeExpiredAuthRateLimits = exports.clearAuthRateLimit = exports.consumeAuthRateLimit = exports.MAXIMUM_PASSWORD_LENGTH = exports.MINIMUM_PASSWORD_LENGTH = exports.validatePassword = exports.verifyPassword = exports.hashPassword = exports.REGISTRY_INSTALLATION_ID = exports.REGISTRY_AUTH_SCHEMA_VERSION = exports.installRegistry = exports.getRegistryInstallationState = exports.getRegistryInstallation = exports.DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS = exports.DEVICE_AUTHORIZATION_LIFETIME_SECONDS = exports.normalizeRegistryUserCode = exports.generateRegistryUserCode = exports.purgeExpiredRegistryDeviceAuthorizations = exports.exchangeRegistryDeviceAuthorization = exports.denyRegistryDeviceAuthorization = exports.approveRegistryDeviceAuthorization = exports.createRegistryDeviceAuthorization = exports.SYNC_SECRET_ENV = exports.resolveRegistrySyncSecret = exports.authenticateRegistrySyncSecret = exports.buildGravatarUrl = exports.secretHashMatches = exports.normalizeEmail = exports.hashSecret = exports.createOpaqueSecret = exports.PASSWORD_RESET_LIFETIME_MS = exports.INVITATION_LIFETIME_MS = exports.resetPassword = exports.createPasswordReset = exports.acceptUserInvitation = exports.resendUserInvitation = exports.createUserInvitation = exports.registryRoleAllowsScopes = exports.resolveRegistryScope = exports.expandRegistryScopes = exports.normalizeRegistryScopes = exports.parseRegistryAccessToken = exports.formatRegistryAccessToken = exports.authenticateRegistryAccessToken = exports.revokeAllRegistryAccessTokens = exports.revokeRegistryAccessToken = exports.listRegistryAccessTokens = exports.createRegistryAccessToken = void 0;
|
|
4
|
+
exports.validateRegistryImageUrl = exports.validateRegistryEmail = exports.validateRegistryDisplayName = exports.isTransmittableCredential = exports.isRegistryTokenPrincipal = exports.registryPrincipalHasScope = exports.scopesForRegistryRole = exports.REGISTRY_WRITE_SCOPE = exports.REGISTRY_READ_SCOPE = exports.setRegistryUserStatus = exports.updateRegistryUserRole = exports.updateRegistryUserProfile = exports.authenticateRegistryCredentials = exports.listRegistryUsers = void 0;
|
|
5
5
|
var access_tokens_1 = require("./access-tokens");
|
|
6
6
|
Object.defineProperty(exports, "createRegistryAccessToken", { enumerable: true, get: function () { return access_tokens_1.createRegistryAccessToken; } });
|
|
7
7
|
Object.defineProperty(exports, "listRegistryAccessTokens", { enumerable: true, get: function () { return access_tokens_1.listRegistryAccessTokens; } });
|
|
@@ -11,6 +11,8 @@ Object.defineProperty(exports, "authenticateRegistryAccessToken", { enumerable:
|
|
|
11
11
|
Object.defineProperty(exports, "formatRegistryAccessToken", { enumerable: true, get: function () { return access_tokens_1.formatRegistryAccessToken; } });
|
|
12
12
|
Object.defineProperty(exports, "parseRegistryAccessToken", { enumerable: true, get: function () { return access_tokens_1.parseRegistryAccessToken; } });
|
|
13
13
|
Object.defineProperty(exports, "normalizeRegistryScopes", { enumerable: true, get: function () { return access_tokens_1.normalizeRegistryScopes; } });
|
|
14
|
+
Object.defineProperty(exports, "expandRegistryScopes", { enumerable: true, get: function () { return access_tokens_1.expandRegistryScopes; } });
|
|
15
|
+
Object.defineProperty(exports, "resolveRegistryScope", { enumerable: true, get: function () { return access_tokens_1.resolveRegistryScope; } });
|
|
14
16
|
Object.defineProperty(exports, "registryRoleAllowsScopes", { enumerable: true, get: function () { return access_tokens_1.registryRoleAllowsScopes; } });
|
|
15
17
|
var action_tokens_1 = require("./action-tokens");
|
|
16
18
|
Object.defineProperty(exports, "createUserInvitation", { enumerable: true, get: function () { return action_tokens_1.createUserInvitation; } });
|
|
@@ -27,6 +29,10 @@ Object.defineProperty(exports, "normalizeEmail", { enumerable: true, get: functi
|
|
|
27
29
|
Object.defineProperty(exports, "secretHashMatches", { enumerable: true, get: function () { return crypto_1.secretHashMatches; } });
|
|
28
30
|
var gravatar_1 = require("./gravatar");
|
|
29
31
|
Object.defineProperty(exports, "buildGravatarUrl", { enumerable: true, get: function () { return gravatar_1.buildGravatarUrl; } });
|
|
32
|
+
var sync_secret_1 = require("./sync-secret");
|
|
33
|
+
Object.defineProperty(exports, "authenticateRegistrySyncSecret", { enumerable: true, get: function () { return sync_secret_1.authenticateRegistrySyncSecret; } });
|
|
34
|
+
Object.defineProperty(exports, "resolveRegistrySyncSecret", { enumerable: true, get: function () { return sync_secret_1.resolveRegistrySyncSecret; } });
|
|
35
|
+
Object.defineProperty(exports, "SYNC_SECRET_ENV", { enumerable: true, get: function () { return sync_secret_1.SYNC_SECRET_ENV; } });
|
|
30
36
|
var device_authorizations_1 = require("./device-authorizations");
|
|
31
37
|
Object.defineProperty(exports, "createRegistryDeviceAuthorization", { enumerable: true, get: function () { return device_authorizations_1.createRegistryDeviceAuthorization; } });
|
|
32
38
|
Object.defineProperty(exports, "approveRegistryDeviceAuthorization", { enumerable: true, get: function () { return device_authorizations_1.approveRegistryDeviceAuthorization; } });
|
|
@@ -66,7 +72,9 @@ Object.defineProperty(exports, "REGISTRY_READ_SCOPE", { enumerable: true, get: f
|
|
|
66
72
|
Object.defineProperty(exports, "REGISTRY_WRITE_SCOPE", { enumerable: true, get: function () { return types_1.REGISTRY_WRITE_SCOPE; } });
|
|
67
73
|
Object.defineProperty(exports, "scopesForRegistryRole", { enumerable: true, get: function () { return types_1.scopesForRegistryRole; } });
|
|
68
74
|
Object.defineProperty(exports, "registryPrincipalHasScope", { enumerable: true, get: function () { return types_1.registryPrincipalHasScope; } });
|
|
75
|
+
Object.defineProperty(exports, "isRegistryTokenPrincipal", { enumerable: true, get: function () { return types_1.isRegistryTokenPrincipal; } });
|
|
69
76
|
var validation_1 = require("./validation");
|
|
77
|
+
Object.defineProperty(exports, "isTransmittableCredential", { enumerable: true, get: function () { return validation_1.isTransmittableCredential; } });
|
|
70
78
|
Object.defineProperty(exports, "validateRegistryDisplayName", { enumerable: true, get: function () { return validation_1.validateRegistryDisplayName; } });
|
|
71
79
|
Object.defineProperty(exports, "validateRegistryEmail", { enumerable: true, get: function () { return validation_1.validateRegistryEmail; } });
|
|
72
80
|
Object.defineProperty(exports, "validateRegistryImageUrl", { enumerable: true, get: function () { return validation_1.validateRegistryImageUrl; } });
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type RegistrySyncSecretPrincipal } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Deployment-wide sync secret.
|
|
4
|
+
*
|
|
5
|
+
* The documented API describes a raw secret carried as a bearer credential for CI-style sync, which is
|
|
6
|
+
* what this module resolves. It is the credential itself, so presenting it issues no token and creates
|
|
7
|
+
* no session: no owner, no expiry, no usage trail, and unsetting the variable is the only way to
|
|
8
|
+
* withdraw it. User-issued `hnd_*` tokens stay the canonical path, and this is off whenever the
|
|
9
|
+
* variable is absent.
|
|
10
|
+
*/
|
|
11
|
+
/** Env var holding the raw secret. Read per call so unsetting it takes effect on restart alone. */
|
|
12
|
+
export declare const SYNC_SECRET_ENV = "HANDOFF_SYNC_SECRET";
|
|
13
|
+
/**
|
|
14
|
+
* The configured secret, or `''` when this method is unavailable. Never returns a value that could not
|
|
15
|
+
* be presented in every accepted header, or one using the access-token prefix: credentials are routed
|
|
16
|
+
* by shape, so such a secret would be permanently unreachable. Both cases warn instead.
|
|
17
|
+
*/
|
|
18
|
+
export declare const resolveRegistrySyncSecret: (env?: NodeJS.ProcessEnv) => string;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a raw credential into a sync-secret principal, granting registry read and write.
|
|
21
|
+
*
|
|
22
|
+
* The empty checks have to come first: `hashSecret('')` is a valid digest that matches itself, so
|
|
23
|
+
* hashing up front would authenticate an empty credential on any deployment that leaves
|
|
24
|
+
* {@link SYNC_SECRET_ENV} unset. The comparison runs over fixed-length digests, so it is constant time
|
|
25
|
+
* and leaks no length.
|
|
26
|
+
*/
|
|
27
|
+
export declare const authenticateRegistrySyncSecret: (credential: string, env?: NodeJS.ProcessEnv) => RegistrySyncSecretPrincipal | null;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.authenticateRegistrySyncSecret = exports.resolveRegistrySyncSecret = exports.SYNC_SECRET_ENV = void 0;
|
|
4
|
+
const crypto_1 = require("./crypto");
|
|
5
|
+
const types_1 = require("./types");
|
|
6
|
+
const validation_1 = require("./validation");
|
|
7
|
+
/**
|
|
8
|
+
* Deployment-wide sync secret.
|
|
9
|
+
*
|
|
10
|
+
* The documented API describes a raw secret carried as a bearer credential for CI-style sync, which is
|
|
11
|
+
* what this module resolves. It is the credential itself, so presenting it issues no token and creates
|
|
12
|
+
* no session: no owner, no expiry, no usage trail, and unsetting the variable is the only way to
|
|
13
|
+
* withdraw it. User-issued `hnd_*` tokens stay the canonical path, and this is off whenever the
|
|
14
|
+
* variable is absent.
|
|
15
|
+
*/
|
|
16
|
+
/** Env var holding the raw secret. Read per call so unsetting it takes effect on restart alone. */
|
|
17
|
+
exports.SYNC_SECRET_ENV = 'HANDOFF_SYNC_SECRET';
|
|
18
|
+
/** Prefix reserved for issued access tokens; see `formatRegistryAccessToken`. */
|
|
19
|
+
const RESERVED_TOKEN_PREFIX = 'hnd_';
|
|
20
|
+
/** Reasons already reported, so a misconfigured deployment warns once rather than once per request. */
|
|
21
|
+
const warned = new Set();
|
|
22
|
+
const warnOnce = (reason, detail) => {
|
|
23
|
+
if (warned.has(reason))
|
|
24
|
+
return;
|
|
25
|
+
warned.add(reason);
|
|
26
|
+
console.warn(`${exports.SYNC_SECRET_ENV} is ignored: ${detail}`);
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The configured secret, or `''` when this method is unavailable. Never returns a value that could not
|
|
30
|
+
* be presented in every accepted header, or one using the access-token prefix: credentials are routed
|
|
31
|
+
* by shape, so such a secret would be permanently unreachable. Both cases warn instead.
|
|
32
|
+
*/
|
|
33
|
+
const resolveRegistrySyncSecret = (env = process.env) => {
|
|
34
|
+
var _a, _b;
|
|
35
|
+
const configured = (_b = (_a = env[exports.SYNC_SECRET_ENV]) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : '';
|
|
36
|
+
if (!configured)
|
|
37
|
+
return '';
|
|
38
|
+
if (!(0, validation_1.isTransmittableCredential)(configured)) {
|
|
39
|
+
warnOnce('untransmittable', 'the value must be printable ASCII with no spaces or commas to be sent in a request header.');
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
if (configured.startsWith(RESERVED_TOKEN_PREFIX)) {
|
|
43
|
+
warnOnce('token_shaped', `the value starts with the \`${RESERVED_TOKEN_PREFIX}\` prefix reserved for issued access tokens.`);
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
return configured;
|
|
47
|
+
};
|
|
48
|
+
exports.resolveRegistrySyncSecret = resolveRegistrySyncSecret;
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a raw credential into a sync-secret principal, granting registry read and write.
|
|
51
|
+
*
|
|
52
|
+
* The empty checks have to come first: `hashSecret('')` is a valid digest that matches itself, so
|
|
53
|
+
* hashing up front would authenticate an empty credential on any deployment that leaves
|
|
54
|
+
* {@link SYNC_SECRET_ENV} unset. The comparison runs over fixed-length digests, so it is constant time
|
|
55
|
+
* and leaks no length.
|
|
56
|
+
*/
|
|
57
|
+
const authenticateRegistrySyncSecret = (credential, env = process.env) => {
|
|
58
|
+
const configured = (0, exports.resolveRegistrySyncSecret)(env);
|
|
59
|
+
if (!configured || !credential)
|
|
60
|
+
return null;
|
|
61
|
+
if (!(0, crypto_1.secretHashMatches)(credential, (0, crypto_1.hashSecret)(configured)))
|
|
62
|
+
return null;
|
|
63
|
+
return { kind: 'sync_secret', scopes: [types_1.REGISTRY_READ_SCOPE, types_1.REGISTRY_WRITE_SCOPE] };
|
|
64
|
+
};
|
|
65
|
+
exports.authenticateRegistrySyncSecret = authenticateRegistrySyncSecret;
|
|
@@ -14,7 +14,9 @@ export interface RegistryUser {
|
|
|
14
14
|
createdAt: Date;
|
|
15
15
|
updatedAt: Date;
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
/** A request authenticated by a user-issued, revocable `hnd_*` access token. */
|
|
18
|
+
export interface RegistryTokenPrincipal {
|
|
19
|
+
kind: 'access_token';
|
|
18
20
|
tokenId: string;
|
|
19
21
|
userId: string;
|
|
20
22
|
email: string;
|
|
@@ -22,5 +24,22 @@ export interface RegistryPrincipal {
|
|
|
22
24
|
role: RegistryUserRole;
|
|
23
25
|
scopes: RegistryAccessScope[];
|
|
24
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* A request authenticated by the deployment-wide `HANDOFF_SYNC_SECRET`. The secret is the credential
|
|
29
|
+
* itself, so there is no token row and no owner, only the scopes it grants. Nothing derived from the
|
|
30
|
+
* secret lives here — no hash, no label: response bodies are serialized verbatim, so any field added
|
|
31
|
+
* to this principal reaches a client as written.
|
|
32
|
+
*/
|
|
33
|
+
export interface RegistrySyncSecretPrincipal {
|
|
34
|
+
kind: 'sync_secret';
|
|
35
|
+
scopes: RegistryAccessScope[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Whoever a registry request is acting as. Discriminated on `kind` because identity fields exist only
|
|
39
|
+
* for token principals, and both tsconfigs run without `strictNullChecks`: one interface with nullable
|
|
40
|
+
* fields would let `principal.userId` compile against a secret-authenticated request.
|
|
41
|
+
*/
|
|
42
|
+
export type RegistryPrincipal = RegistryTokenPrincipal | RegistrySyncSecretPrincipal;
|
|
43
|
+
export declare const isRegistryTokenPrincipal: (principal: RegistryPrincipal) => principal is RegistryTokenPrincipal;
|
|
25
44
|
export declare const scopesForRegistryRole: (role: RegistryUserRole) => RegistryAccessScope[];
|
|
26
45
|
export declare const registryPrincipalHasScope: (principal: RegistryPrincipal, scope: RegistryAccessScope) => boolean;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.registryPrincipalHasScope = exports.scopesForRegistryRole = exports.REGISTRY_WRITE_SCOPE = exports.REGISTRY_READ_SCOPE = void 0;
|
|
3
|
+
exports.registryPrincipalHasScope = exports.scopesForRegistryRole = exports.isRegistryTokenPrincipal = exports.REGISTRY_WRITE_SCOPE = exports.REGISTRY_READ_SCOPE = void 0;
|
|
4
4
|
exports.REGISTRY_READ_SCOPE = 'registry:read';
|
|
5
5
|
exports.REGISTRY_WRITE_SCOPE = 'registry:write';
|
|
6
|
+
const isRegistryTokenPrincipal = (principal) => principal.kind === 'access_token';
|
|
7
|
+
exports.isRegistryTokenPrincipal = isRegistryTokenPrincipal;
|
|
6
8
|
const scopesForRegistryRole = (role) => role === 'admin' ? [exports.REGISTRY_READ_SCOPE, exports.REGISTRY_WRITE_SCOPE] : [exports.REGISTRY_READ_SCOPE];
|
|
7
9
|
exports.scopesForRegistryRole = scopesForRegistryRole;
|
|
8
10
|
const registryPrincipalHasScope = (principal, scope) => principal.scopes.includes(scope);
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a credential can be presented unambiguously in a request header. One rule shared by the
|
|
3
|
+
* authentication headers and by whatever a deployment configures as a secret, so a value is never
|
|
4
|
+
* accepted through one carrier and rejected by the other.
|
|
5
|
+
*
|
|
6
|
+
* Whitespace is out so nothing can be trimmed away or smuggled past a parser, and the comma with it:
|
|
7
|
+
* repeats of a header Node does not de-duplicate arrive joined with `, `, and no accepted credential
|
|
8
|
+
* may look like that. Non-ASCII bytes are out too, since their header encoding is ambiguous and a
|
|
9
|
+
* credential has to reproduce exactly.
|
|
10
|
+
*/
|
|
11
|
+
export declare const isTransmittableCredential: (value: string) => boolean;
|
|
1
12
|
export declare const validateRegistryEmail: (email: string) => string | null;
|
|
2
13
|
export declare const validateRegistryDisplayName: (name: string) => string | null;
|
|
3
14
|
export declare const validateRegistryImageUrl: (image: string | null | undefined) => string | null | false;
|
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.validateRegistryImageUrl = exports.validateRegistryDisplayName = exports.validateRegistryEmail = void 0;
|
|
3
|
+
exports.validateRegistryImageUrl = exports.validateRegistryDisplayName = exports.validateRegistryEmail = exports.isTransmittableCredential = void 0;
|
|
4
4
|
const crypto_1 = require("./crypto");
|
|
5
5
|
const SIMPLE_EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6
|
+
/**
|
|
7
|
+
* Printable ASCII (`!` through `~`) except the comma: HTTP's `VCHAR`, minus the character Node joins
|
|
8
|
+
* duplicate header values with.
|
|
9
|
+
*/
|
|
10
|
+
const TRANSMITTABLE_CREDENTIAL_PATTERN = /^[\x21-\x2B\x2D-\x7E]+$/;
|
|
11
|
+
/**
|
|
12
|
+
* Whether a credential can be presented unambiguously in a request header. One rule shared by the
|
|
13
|
+
* authentication headers and by whatever a deployment configures as a secret, so a value is never
|
|
14
|
+
* accepted through one carrier and rejected by the other.
|
|
15
|
+
*
|
|
16
|
+
* Whitespace is out so nothing can be trimmed away or smuggled past a parser, and the comma with it:
|
|
17
|
+
* repeats of a header Node does not de-duplicate arrive joined with `, `, and no accepted credential
|
|
18
|
+
* may look like that. Non-ASCII bytes are out too, since their header encoding is ambiguous and a
|
|
19
|
+
* credential has to reproduce exactly.
|
|
20
|
+
*/
|
|
21
|
+
const isTransmittableCredential = (value) => TRANSMITTABLE_CREDENTIAL_PATTERN.test(value);
|
|
22
|
+
exports.isTransmittableCredential = isTransmittableCredential;
|
|
6
23
|
const validateRegistryEmail = (email) => {
|
|
7
24
|
const normalized = (0, crypto_1.normalizeEmail)(email);
|
|
8
25
|
return normalized.length <= 320 && SIMPLE_EMAIL_PATTERN.test(normalized) ? normalized : null;
|
|
@@ -13,4 +13,4 @@ import Handoff from '../../index';
|
|
|
13
13
|
* Checkout all published asset collections, or a single collection when `collection` is given.
|
|
14
14
|
* Recreates workspace asset files; prompts before overwriting changed local files unless `--force`.
|
|
15
15
|
*/
|
|
16
|
-
export declare const checkoutAssets: (handoff: Handoff,
|
|
16
|
+
export declare const checkoutAssets: (handoff: Handoff, selection?: string[]) => Promise<void>;
|
|
@@ -84,15 +84,19 @@ const wouldChange = (absolutePath, bytes) => {
|
|
|
84
84
|
* Checkout all published asset collections, or a single collection when `collection` is given.
|
|
85
85
|
* Recreates workspace asset files; prompts before overwriting changed local files unless `--force`.
|
|
86
86
|
*/
|
|
87
|
-
const checkoutAssets = (handoff,
|
|
87
|
+
const checkoutAssets = (handoff, selection) => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
+
var _a;
|
|
88
89
|
const connection = yield (0, index_1.resolveConnectionOrThrow)(handoff);
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
const unsupported = (_a = selection === null || selection === void 0 ? void 0 : selection.filter((name) => !(0, sets_1.isAssetCollection)(name))) !== null && _a !== void 0 ? _a : [];
|
|
91
|
+
if (unsupported.length > 0) {
|
|
92
|
+
throw new index_1.CheckoutError(`Unknown asset collection ${unsupported.map((name) => `"${name}"`).join(', ')}. ` +
|
|
93
|
+
`Supported collections: ${sets_1.ASSET_COLLECTIONS.join(', ')}.`);
|
|
91
94
|
}
|
|
92
95
|
const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
|
|
96
|
+
// A named selection is trusted as-is; only a full checkout has to ask the registry what exists.
|
|
93
97
|
let collections;
|
|
94
|
-
if (
|
|
95
|
-
collections =
|
|
98
|
+
if (selection) {
|
|
99
|
+
collections = selection;
|
|
96
100
|
}
|
|
97
101
|
else {
|
|
98
102
|
const summaries = yield client.listAssetCollections();
|
|
@@ -123,16 +127,27 @@ const checkoutAssets = (handoff, collection) => __awaiter(void 0, void 0, void 0
|
|
|
123
127
|
// Download each referenced blob once (dedup by hash), then resolve every asset to a physical write.
|
|
124
128
|
const blobCache = new Map();
|
|
125
129
|
const writes = [];
|
|
130
|
+
const plannedPaths = [];
|
|
126
131
|
for (const payload of payloads) {
|
|
127
132
|
for (const asset of payload.assets) {
|
|
133
|
+
const absolutePath = (0, layout_1.resolveAssetPhysicalPath)(asset.path, roots);
|
|
134
|
+
plannedPaths.push(absolutePath);
|
|
135
|
+
// A dry run reports paths only, so there is no reason to pull the bytes down.
|
|
136
|
+
if (handoff.dryRun) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
128
139
|
let bytes = blobCache.get(asset.contentHash);
|
|
129
140
|
if (!bytes) {
|
|
130
141
|
bytes = yield client.downloadAssetBlob(asset.contentHash);
|
|
131
142
|
blobCache.set(asset.contentHash, bytes);
|
|
132
143
|
}
|
|
133
|
-
writes.push({ absolutePath
|
|
144
|
+
writes.push({ absolutePath, bytes });
|
|
134
145
|
}
|
|
135
146
|
}
|
|
147
|
+
if (handoff.dryRun) {
|
|
148
|
+
(0, index_1.reportPlannedWrites)(handoff, 'asset collections', plannedPaths, plannedPaths.filter((target) => fs_extra_1.default.existsSync(target)));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
136
151
|
const existingChanges = writes.filter((write) => fs_extra_1.default.existsSync(write.absolutePath) && wouldChange(write.absolutePath, write.bytes));
|
|
137
152
|
if (existingChanges.length > 0 && !handoff.force) {
|
|
138
153
|
const proceed = yield p.confirm({
|
|
@@ -24,6 +24,13 @@ export declare class CheckoutError extends Error {
|
|
|
24
24
|
* actionable {@link CheckoutError} naming the exact misconfiguration.
|
|
25
25
|
*/
|
|
26
26
|
export declare const resolveConnectionOrThrow: (handoff: Handoff) => Promise<import("../connection").ResolvedRegistryConnection>;
|
|
27
|
+
/**
|
|
28
|
+
* Report the files a checkout would create or replace, and write nothing. Called once the real
|
|
29
|
+
* checkout has computed its targets but before it prompts or writes, so a dry run exercises the same
|
|
30
|
+
* path resolution and conflict detection as the real thing. Shared by the entity, page, token and
|
|
31
|
+
* asset checkout paths.
|
|
32
|
+
*/
|
|
33
|
+
export declare const reportPlannedWrites: (handoff: Handoff, label: string, targets: string[], conflicts: string[]) => void;
|
|
27
34
|
/**
|
|
28
35
|
* Checkout a single component, pattern, or page from the connected registry into this workspace.
|
|
29
36
|
* Overwriting existing local files requires `--force` or an interactive confirmation.
|
|
@@ -35,4 +42,4 @@ export declare const checkoutEntity: (handoff: Handoff, kind: TransferEntityKind
|
|
|
35
42
|
* is collected and never aborts the rest; the run throws at the end if any entity failed. Overwrite
|
|
36
43
|
* prompting is per entity (skipped under `--force`).
|
|
37
44
|
*/
|
|
38
|
-
export declare const checkoutEntities: (handoff: Handoff, kind: TransferEntityKind) => Promise<void>;
|
|
45
|
+
export declare const checkoutEntities: (handoff: Handoff, kind: TransferEntityKind, ids?: string[]) => Promise<void>;
|
|
@@ -60,7 +60,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
60
60
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
61
61
|
};
|
|
62
62
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
63
|
-
exports.checkoutEntities = exports.checkoutEntity = exports.resolveConnectionOrThrow = exports.CheckoutError = void 0;
|
|
63
|
+
exports.checkoutEntities = exports.checkoutEntity = exports.reportPlannedWrites = exports.resolveConnectionOrThrow = exports.CheckoutError = void 0;
|
|
64
64
|
const p = __importStar(require("@clack/prompts"));
|
|
65
65
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
66
66
|
const path_1 = __importDefault(require("path"));
|
|
@@ -70,6 +70,7 @@ const logger_1 = require("../../utils/logger");
|
|
|
70
70
|
const client_1 = require("../client");
|
|
71
71
|
const connection_1 = require("../connection");
|
|
72
72
|
const path_2 = require("../path");
|
|
73
|
+
const selection_1 = require("../selection");
|
|
73
74
|
/** A connected-workspace configuration or precondition failure surfaced to the CLI. */
|
|
74
75
|
class CheckoutError extends Error {
|
|
75
76
|
constructor(message) {
|
|
@@ -568,6 +569,21 @@ const writeSourceFile = (targetDir, file) => __awaiter(void 0, void 0, void 0, f
|
|
|
568
569
|
yield fs_extra_1.default.writeFile(absolutePath, file.content, 'utf8');
|
|
569
570
|
return absolutePath;
|
|
570
571
|
});
|
|
572
|
+
/**
|
|
573
|
+
* Report the files a checkout would create or replace, and write nothing. Called once the real
|
|
574
|
+
* checkout has computed its targets but before it prompts or writes, so a dry run exercises the same
|
|
575
|
+
* path resolution and conflict detection as the real thing. Shared by the entity, page, token and
|
|
576
|
+
* asset checkout paths.
|
|
577
|
+
*/
|
|
578
|
+
const reportPlannedWrites = (handoff, label, targets, conflicts) => {
|
|
579
|
+
const conflicting = new Set(conflicts);
|
|
580
|
+
logger_1.Logger.info(`Would checkout ${label} (${targets.length} file(s)):`);
|
|
581
|
+
for (const target of targets) {
|
|
582
|
+
const relative = path_1.default.relative(handoff.workingPath, target) || target;
|
|
583
|
+
logger_1.Logger.info(` ${conflicting.has(target) ? 'overwrite' : 'create'}: ${relative}`);
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
exports.reportPlannedWrites = reportPlannedWrites;
|
|
571
587
|
/**
|
|
572
588
|
* Checkout a page: write its single verbatim `.md` (transfer path `<id>.md`) back under
|
|
573
589
|
* `<workingPath>/pages/`. Unlike components/patterns there is no declaration to synthesize — the
|
|
@@ -583,6 +599,10 @@ const checkoutPage = (handoff, id, payload) => __awaiter(void 0, void 0, void 0,
|
|
|
583
599
|
return target;
|
|
584
600
|
});
|
|
585
601
|
const conflicts = targets.filter((file) => fs_extra_1.default.existsSync(file));
|
|
602
|
+
if (handoff.dryRun) {
|
|
603
|
+
(0, exports.reportPlannedWrites)(handoff, `page "${id}"`, targets, conflicts);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
586
606
|
if (!(yield confirmOverwrite(handoff, conflicts))) {
|
|
587
607
|
logger_1.Logger.warn(`Checkout of page "${id}" cancelled; no files were written.`);
|
|
588
608
|
return;
|
|
@@ -633,7 +653,13 @@ const checkoutSingle = (handoff, kind, id, client, registryUrl) => __awaiter(voi
|
|
|
633
653
|
}
|
|
634
654
|
return { file, absolutePath };
|
|
635
655
|
});
|
|
636
|
-
const
|
|
656
|
+
const plannedWrites = [...sourceTargets.map((entry) => entry.absolutePath), declarationPath];
|
|
657
|
+
const conflicts = plannedWrites.filter((file) => fs_extra_1.default.existsSync(file));
|
|
658
|
+
if (handoff.dryRun) {
|
|
659
|
+
// Returning null also skips `registerCheckedOut`, so a dry run never edits handoff.config either.
|
|
660
|
+
(0, exports.reportPlannedWrites)(handoff, `${kind} "${id}"`, plannedWrites, conflicts);
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
637
663
|
if (!(yield confirmOverwrite(handoff, conflicts))) {
|
|
638
664
|
logger_1.Logger.warn(`Checkout of ${kind} "${id}" cancelled; no files were written.`);
|
|
639
665
|
return null;
|
|
@@ -694,7 +720,7 @@ exports.checkoutEntity = checkoutEntity;
|
|
|
694
720
|
* is collected and never aborts the rest; the run throws at the end if any entity failed. Overwrite
|
|
695
721
|
* prompting is per entity (skipped under `--force`).
|
|
696
722
|
*/
|
|
697
|
-
const checkoutEntities = (handoff, kind) => __awaiter(void 0, void 0, void 0, function* () {
|
|
723
|
+
const checkoutEntities = (handoff, kind, ids) => __awaiter(void 0, void 0, void 0, function* () {
|
|
698
724
|
const connection = yield (0, exports.resolveConnectionOrThrow)(handoff);
|
|
699
725
|
const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
|
|
700
726
|
let summaries;
|
|
@@ -707,14 +733,20 @@ const checkoutEntities = (handoff, kind) => __awaiter(void 0, void 0, void 0, fu
|
|
|
707
733
|
}
|
|
708
734
|
throw error;
|
|
709
735
|
}
|
|
710
|
-
|
|
736
|
+
const published = summaries.map((summary) => summary.id);
|
|
737
|
+
const { selected: targets, unknown } = (0, selection_1.selectIds)(published, ids);
|
|
738
|
+
if (unknown.length > 0) {
|
|
739
|
+
throw new CheckoutError(`No ${kind} named ${unknown.map((id) => `"${id}"`).join(', ')} is published in the registry. ` +
|
|
740
|
+
`Published ${kind}s: ${published.join(', ') || '(none)'}.`);
|
|
741
|
+
}
|
|
742
|
+
if (targets.length === 0) {
|
|
711
743
|
logger_1.Logger.success(`No ${kind}s are published in the registry; nothing to checkout.`);
|
|
712
744
|
return;
|
|
713
745
|
}
|
|
714
746
|
let checkedOut = 0;
|
|
715
747
|
const failed = [];
|
|
716
748
|
const targetDirs = [];
|
|
717
|
-
for (const
|
|
749
|
+
for (const id of targets) {
|
|
718
750
|
try {
|
|
719
751
|
const targetDir = yield checkoutSingle(handoff, kind, id, client, connection.url);
|
|
720
752
|
if (targetDir) {
|
|
@@ -728,7 +760,8 @@ const checkoutEntities = (handoff, kind) => __awaiter(void 0, void 0, void 0, fu
|
|
|
728
760
|
}
|
|
729
761
|
// Register everything checked out in one pass, so the config is edited (and confirmed) once.
|
|
730
762
|
yield registerCheckedOut(handoff, kind, targetDirs);
|
|
731
|
-
logger_1.Logger.success(`${kind[0].toUpperCase()}${kind.slice(1)}s checkout complete — ${checkedOut}
|
|
763
|
+
logger_1.Logger.success(`${kind[0].toUpperCase()}${kind.slice(1)}s checkout complete — ${checkedOut} ${handoff.dryRun ? 'would be checked out' : 'checked out'}` +
|
|
764
|
+
`${failed.length ? `, ${failed.length} failed` : ''}.`);
|
|
732
765
|
if (failed.length > 0) {
|
|
733
766
|
for (const failure of failed) {
|
|
734
767
|
logger_1.Logger.error(` - ${failure.id}: ${failure.message}`);
|
|
@@ -12,4 +12,4 @@ import Handoff from '../../index';
|
|
|
12
12
|
* Checkout all published token sets, or a single set when `setId` is given. Reconstructs `tokens.json`
|
|
13
13
|
* and restores generated files; prompts before overwriting changed local files unless `--force`.
|
|
14
14
|
*/
|
|
15
|
-
export declare const checkoutTokens: (handoff: Handoff,
|
|
15
|
+
export declare const checkoutTokens: (handoff: Handoff, selection?: string[]) => Promise<void>;
|
|
@@ -97,15 +97,18 @@ const wouldChange = (absolutePath, content) => {
|
|
|
97
97
|
* Checkout all published token sets, or a single set when `setId` is given. Reconstructs `tokens.json`
|
|
98
98
|
* and restores generated files; prompts before overwriting changed local files unless `--force`.
|
|
99
99
|
*/
|
|
100
|
-
const checkoutTokens = (handoff,
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
const checkoutTokens = (handoff, selection) => __awaiter(void 0, void 0, void 0, function* () {
|
|
101
|
+
var _a;
|
|
102
|
+
const unsupported = (_a = selection === null || selection === void 0 ? void 0 : selection.filter((id) => !(0, sets_1.isTokenSetId)(id))) !== null && _a !== void 0 ? _a : [];
|
|
103
|
+
if (unsupported.length > 0) {
|
|
104
|
+
throw new index_1.CheckoutError(`Unknown or unsafe token set id ${unsupported.map((id) => `"${id}"`).join(', ')}.`);
|
|
103
105
|
}
|
|
104
106
|
const connection = yield (0, index_1.resolveConnectionOrThrow)(handoff);
|
|
105
107
|
const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
|
|
108
|
+
// A named selection is trusted as-is; only a full checkout has to ask the registry what exists.
|
|
106
109
|
let ids;
|
|
107
|
-
if (
|
|
108
|
-
ids =
|
|
110
|
+
if (selection) {
|
|
111
|
+
ids = selection;
|
|
109
112
|
}
|
|
110
113
|
else {
|
|
111
114
|
const summaries = yield client.listTokenSets();
|
|
@@ -153,6 +156,10 @@ const checkoutTokens = (handoff, setId) => __awaiter(void 0, void 0, void 0, fun
|
|
|
153
156
|
}
|
|
154
157
|
}
|
|
155
158
|
const existingChanges = changed.filter((filePath) => fs_extra_1.default.existsSync(filePath));
|
|
159
|
+
if (handoff.dryRun) {
|
|
160
|
+
(0, index_1.reportPlannedWrites)(handoff, 'token sets', changed, existingChanges);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
156
163
|
if (existingChanges.length > 0 && !handoff.force) {
|
|
157
164
|
const proceed = yield p.confirm({
|
|
158
165
|
message: `Checkout will overwrite ${existingChanges.length} changed local token file(s). Continue?`,
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* config + environment: the registry URL may be given inline or via an env-var *name*, and the
|
|
7
7
|
* access token is always read from an env-var name (never persisted as a value). This is the single
|
|
8
8
|
* source the publish (and later checkout) client reads its target + credentials from.
|
|
9
|
+
*
|
|
10
|
+
* The older `HANDOFF_CLOUD_*` names still work as deprecated fallbacks; see
|
|
11
|
+
* {@link DEPRECATED_ENV_ALIASES}.
|
|
9
12
|
*/
|
|
10
13
|
import type { Config } from '../types/config';
|
|
11
14
|
/** Default env-var name holding the remote registry base URL. */
|
|
@@ -26,6 +29,9 @@ export interface ResolvedRegistryConnection {
|
|
|
26
29
|
/**
|
|
27
30
|
* Resolve the connected-workspace registry connection from config + environment. Never throws —
|
|
28
31
|
* Callers inspect the resolved URL and access token to surface specific configuration errors.
|
|
32
|
+
*
|
|
33
|
+
* `urlEnv` and `accessTokenEnv` always report the canonical name, even when an alias supplied the
|
|
34
|
+
* value, so errors built from them point at the variable to adopt.
|
|
29
35
|
*/
|
|
30
36
|
export declare const resolveRegistryConnection: (config: Config | null | undefined) => ResolvedRegistryConnection;
|
|
31
37
|
/**
|
|
@@ -35,5 +41,9 @@ export declare const resolveRegistryConnection: (config: Config | null | undefin
|
|
|
35
41
|
* keep precedence for deterministic CI: `HANDOFF_REGISTRY_URL` (or inline config) wins over the
|
|
36
42
|
* saved URL, and `HANDOFF_REGISTRY_ACCESS_TOKEN` wins over the saved token. The saved token only
|
|
37
43
|
* applies to the URL it was issued for.
|
|
44
|
+
*
|
|
45
|
+
* A configured `HANDOFF_SYNC_SECRET` is the last resort. Being deployment-wide rather than personal,
|
|
46
|
+
* it fills in only where there is no explicit token and no login (CI, in practice), instead of quietly
|
|
47
|
+
* speaking for a developer who is signed in.
|
|
38
48
|
*/
|
|
39
49
|
export declare const resolveAuthenticatedRegistryConnection: (config: Config | null | undefined, workingPath?: string) => Promise<ResolvedRegistryConnection>;
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* config + environment: the registry URL may be given inline or via an env-var *name*, and the
|
|
8
8
|
* access token is always read from an env-var name (never persisted as a value). This is the single
|
|
9
9
|
* source the publish (and later checkout) client reads its target + credentials from.
|
|
10
|
+
*
|
|
11
|
+
* The older `HANDOFF_CLOUD_*` names still work as deprecated fallbacks; see
|
|
12
|
+
* {@link DEPRECATED_ENV_ALIASES}.
|
|
10
13
|
*/
|
|
11
14
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
12
15
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
@@ -20,22 +23,60 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
20
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
24
|
exports.resolveAuthenticatedRegistryConnection = exports.resolveRegistryConnection = exports.DEFAULT_REGISTRY_ACCESS_TOKEN_ENV = exports.DEFAULT_REGISTRY_URL_ENV = void 0;
|
|
22
25
|
const store_1 = require("../cli/auth/store");
|
|
26
|
+
// Imported straight from the module, not the `auth` barrel, to keep the database schema off the CLI
|
|
27
|
+
// startup path; this file is resolved for every command.
|
|
28
|
+
const sync_secret_1 = require("./auth/sync-secret");
|
|
29
|
+
const logger_1 = require("../utils/logger");
|
|
23
30
|
/** Default env-var name holding the remote registry base URL. */
|
|
24
31
|
exports.DEFAULT_REGISTRY_URL_ENV = 'HANDOFF_REGISTRY_URL';
|
|
25
32
|
/** Default env-var name holding the connected-workspace access token. */
|
|
26
33
|
exports.DEFAULT_REGISTRY_ACCESS_TOKEN_ENV = 'HANDOFF_REGISTRY_ACCESS_TOKEN';
|
|
34
|
+
/**
|
|
35
|
+
* Old env-var names we still read, keyed by the name that replaced them. Keeps workspaces set up
|
|
36
|
+
* from the older docs working instead of silently reporting no registry.
|
|
37
|
+
*/
|
|
38
|
+
const DEPRECATED_ENV_ALIASES = {
|
|
39
|
+
[exports.DEFAULT_REGISTRY_URL_ENV]: 'HANDOFF_CLOUD_URL',
|
|
40
|
+
[exports.DEFAULT_REGISTRY_ACCESS_TOKEN_ENV]: 'HANDOFF_CLOUD_TOKEN',
|
|
41
|
+
};
|
|
42
|
+
/** Aliases we've already warned about, so each one warns once per run and not once per lookup. */
|
|
43
|
+
const warnedAliases = new Set();
|
|
44
|
+
/**
|
|
45
|
+
* Value of the named env var, falling back to its deprecated alias. Only the default names have
|
|
46
|
+
* aliases: a custom `urlEnv`/`accessTokenEnv` is a deliberate choice, so a stray `HANDOFF_CLOUD_*`
|
|
47
|
+
* must not stand in for it. The warning goes through {@link Logger.warn}, so
|
|
48
|
+
* `HANDOFF_LOG_LEVEL=error` silences it in CI.
|
|
49
|
+
*/
|
|
50
|
+
const readConnectionEnv = (name) => {
|
|
51
|
+
var _a, _b;
|
|
52
|
+
const configured = (_a = process.env[name]) === null || _a === void 0 ? void 0 : _a.trim();
|
|
53
|
+
if (configured)
|
|
54
|
+
return configured;
|
|
55
|
+
const alias = DEPRECATED_ENV_ALIASES[name];
|
|
56
|
+
const inherited = alias ? (_b = process.env[alias]) === null || _b === void 0 ? void 0 : _b.trim() : '';
|
|
57
|
+
if (!inherited)
|
|
58
|
+
return '';
|
|
59
|
+
if (!warnedAliases.has(alias)) {
|
|
60
|
+
warnedAliases.add(alias);
|
|
61
|
+
logger_1.Logger.warn(`${alias} is deprecated and will be removed in a future release; set ${name} instead.`);
|
|
62
|
+
}
|
|
63
|
+
return inherited;
|
|
64
|
+
};
|
|
27
65
|
/**
|
|
28
66
|
* Resolve the connected-workspace registry connection from config + environment. Never throws —
|
|
29
67
|
* Callers inspect the resolved URL and access token to surface specific configuration errors.
|
|
68
|
+
*
|
|
69
|
+
* `urlEnv` and `accessTokenEnv` always report the canonical name, even when an alias supplied the
|
|
70
|
+
* value, so errors built from them point at the variable to adopt.
|
|
30
71
|
*/
|
|
31
72
|
const resolveRegistryConnection = (config) => {
|
|
32
|
-
var _a, _b, _c, _d
|
|
73
|
+
var _a, _b, _c, _d;
|
|
33
74
|
const connection = (_a = config === null || config === void 0 ? void 0 : config.runtime) === null || _a === void 0 ? void 0 : _a.registryConnection;
|
|
34
75
|
const urlEnv = ((_b = connection === null || connection === void 0 ? void 0 : connection.urlEnv) === null || _b === void 0 ? void 0 : _b.trim()) || exports.DEFAULT_REGISTRY_URL_ENV;
|
|
35
76
|
const accessTokenEnv = ((_c = connection === null || connection === void 0 ? void 0 : connection.accessTokenEnv) === null || _c === void 0 ? void 0 : _c.trim()) || exports.DEFAULT_REGISTRY_ACCESS_TOKEN_ENV;
|
|
36
77
|
const inlineUrl = (_d = connection === null || connection === void 0 ? void 0 : connection.url) === null || _d === void 0 ? void 0 : _d.trim();
|
|
37
|
-
const url =
|
|
38
|
-
const accessToken = (
|
|
78
|
+
const url = inlineUrl || readConnectionEnv(urlEnv);
|
|
79
|
+
const accessToken = readConnectionEnv(accessTokenEnv);
|
|
39
80
|
return {
|
|
40
81
|
url,
|
|
41
82
|
accessToken,
|
|
@@ -51,14 +92,18 @@ exports.resolveRegistryConnection = resolveRegistryConnection;
|
|
|
51
92
|
* keep precedence for deterministic CI: `HANDOFF_REGISTRY_URL` (or inline config) wins over the
|
|
52
93
|
* saved URL, and `HANDOFF_REGISTRY_ACCESS_TOKEN` wins over the saved token. The saved token only
|
|
53
94
|
* applies to the URL it was issued for.
|
|
95
|
+
*
|
|
96
|
+
* A configured `HANDOFF_SYNC_SECRET` is the last resort. Being deployment-wide rather than personal,
|
|
97
|
+
* it fills in only where there is no explicit token and no login (CI, in practice), instead of quietly
|
|
98
|
+
* speaking for a developer who is signed in.
|
|
54
99
|
*/
|
|
55
100
|
const resolveAuthenticatedRegistryConnection = (config_1, ...args_1) => __awaiter(void 0, [config_1, ...args_1], void 0, function* (config, workingPath = process.cwd()) {
|
|
56
101
|
const connection = (0, exports.resolveRegistryConnection)(config);
|
|
57
102
|
const auth = yield (0, store_1.readCliAuth)(workingPath);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const accessToken = connection.accessToken || (
|
|
103
|
+
const login = (0, store_1.cliAuthTokenIsValid)(auth) ? auth : null;
|
|
104
|
+
const url = connection.url || (login === null || login === void 0 ? void 0 : login.remoteUrl) || '';
|
|
105
|
+
const savedToken = login && (0, store_1.cliAuthMatchesRegistry)(login, url) ? login.accessToken : '';
|
|
106
|
+
const accessToken = connection.accessToken || savedToken || (0, sync_secret_1.resolveRegistrySyncSecret)();
|
|
62
107
|
return Object.assign(Object.assign({}, connection), { url, accessToken });
|
|
63
108
|
});
|
|
64
109
|
exports.resolveAuthenticatedRegistryConnection = resolveAuthenticatedRegistryConnection;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logical content-kind vocabulary: the dependency-light boundary shared by the CLI, the
|
|
3
|
+
* publish/checkout orchestration, and the compatibility layer.
|
|
4
|
+
*
|
|
5
|
+
* A workspace publishes and checks out five kinds of content. Components, patterns and pages are
|
|
6
|
+
* entities with a singular wire kind ({@link TransferEntityKind}); tokens and assets travel through
|
|
7
|
+
* their own transfer endpoints. This module owns only the stable plural vocabulary and its ordering,
|
|
8
|
+
* so it stays free of fs, DB and React dependencies.
|
|
9
|
+
*/
|
|
10
|
+
import type { TransferEntityKind } from './transfer';
|
|
11
|
+
/** Stable identity used to publish `pages/index.md` while serving it at the root route. */
|
|
12
|
+
export declare const HOME_PAGE_ID = "index";
|
|
13
|
+
export declare const HOME_PAGE_PATH = "/";
|
|
14
|
+
/** Plural content kinds accepted wherever a publish or checkout target is named. */
|
|
15
|
+
export declare const REGISTRY_ENTITY_KINDS: readonly ["components", "patterns", "pages", "tokens", "assets"];
|
|
16
|
+
export type RegistryEntityKind = (typeof REGISTRY_ENTITY_KINDS)[number];
|
|
17
|
+
/** Map the plural content kind to the singular wire kind used by entity publish/checkout. */
|
|
18
|
+
export declare const ENTITY_WIRE_KIND: Record<'components' | 'patterns' | 'pages', TransferEntityKind>;
|
|
19
|
+
/** The entity kinds, in the order a full run visits them: patterns compose components, pages reference both. */
|
|
20
|
+
export declare const ENTITY_KIND_ORDER: readonly ["components", "patterns", "pages"];
|
|
21
|
+
/**
|
|
22
|
+
* Every kind, in dependency order. Tokens and assets come first because rendered component
|
|
23
|
+
* artifacts reference the CSS variables and asset URLs they produce.
|
|
24
|
+
*/
|
|
25
|
+
export declare const ALL_KIND_ORDER: readonly RegistryEntityKind[];
|
|
26
|
+
/** Whether a string names a supported content kind. */
|
|
27
|
+
export declare const isRegistryEntityKind: (value: string) => value is RegistryEntityKind;
|