handoff-app 2.0.0-8 → 2.0.0
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/{Changelog.md → CHANGELOG.md} +85 -1
- package/{License.md → LICENSE.md} +1 -1
- package/README.md +816 -393
- package/RECIPES.md +975 -0
- package/UPGRADE.md +57 -9
- package/components.json +20 -20
- package/config/docs/foundations/colors/guidelines.md +0 -1
- package/config/docs/foundations/colors.md +0 -1
- package/config/docs/foundations/effects/guidelines.md +0 -1
- package/config/docs/foundations/effects/resources.md +0 -1
- package/config/docs/foundations/effects.md +0 -1
- package/config/docs/foundations/fonts.md +0 -1
- package/config/docs/foundations/grid.md +0 -1
- package/config/docs/foundations/icon.md +0 -1
- package/config/docs/foundations/icons/guidelines.md +0 -1
- package/config/docs/foundations/icons.md +0 -1
- package/config/docs/foundations/images/guidelines.md +0 -1
- package/config/docs/foundations/images/resources.md +0 -1
- package/config/docs/foundations/images.md +0 -1
- package/config/docs/foundations/logo/resources.md +0 -1
- package/config/docs/foundations/logo.md +0 -1
- package/config/docs/foundations/typography/guidelines.md +0 -1
- package/config/docs/foundations/typography/resources.md +0 -1
- package/config/docs/foundations/typography.md +0 -1
- package/config/docs/foundations.md +0 -1
- package/config/docs/guidelines.md +0 -1
- package/config/docs/index.md +0 -1
- package/config/docs/system/tokens/foundations/colors.md +0 -4
- package/config/docs/system/tokens/foundations/effects.md +0 -4
- package/config/docs/system/tokens/foundations/typography.md +0 -2
- package/config/docs/system.md +0 -2
- package/config/templates/page.md +2 -1
- package/config/templates/page.mdx +2 -1
- package/dist/ai/connections.d.ts +66 -0
- package/dist/ai/connections.js +149 -0
- package/dist/ai/define.d.ts +10 -0
- package/dist/ai/define.js +13 -0
- package/dist/ai/types.d.ts +26 -0
- package/dist/ai/types.js +10 -0
- package/dist/app-builder/build.d.ts +14 -7
- package/dist/app-builder/build.js +195 -66
- package/dist/app-builder/client-config.js +3 -2
- package/dist/app-builder/paths.d.ts +0 -12
- package/dist/app-builder/paths.js +3 -20
- package/dist/app-builder/static-export.js +1 -1
- package/dist/app-builder/vercel-output.d.ts +1 -1
- package/dist/app-builder/vercel-output.js +5 -4
- package/dist/cache/build-cache.d.ts +27 -4
- package/dist/cache/build-cache.js +114 -47
- package/dist/cache/build-inputs.d.ts +13 -0
- package/dist/cache/build-inputs.js +19 -0
- package/dist/cache/index.d.ts +2 -1
- package/dist/cache/index.js +5 -1
- package/dist/catalog/normalize.js +3 -0
- package/dist/catalog/previews.js +5 -4
- package/dist/catalog/types.d.ts +8 -0
- package/dist/cli/create.js +1 -2
- package/dist/commands/build/index.d.ts +1 -1
- package/dist/commands/build/index.js +1 -1
- package/dist/commands/utils.d.ts +3 -3
- package/dist/config/defaults.js +7 -4
- package/dist/config/from-env.d.ts +4 -0
- package/dist/config/from-env.js +38 -1
- package/dist/config/helpers.js +2 -2
- package/dist/config/index.d.ts +0 -1
- package/dist/config/index.js +1 -4
- package/dist/config/loader.js +28 -2
- package/dist/config/resolve-env.d.ts +2 -1
- package/dist/config/resolve-env.js +28 -12
- package/dist/config/runtime.js +9 -11
- package/dist/handlebars.d.ts +10 -2
- package/dist/index.d.ts +12 -7
- package/dist/index.js +18 -22
- package/dist/nav/index.d.ts +2 -2
- package/dist/nav/registry.d.ts +3 -3
- package/dist/nav/registry.js +1 -1
- package/dist/nav/workspace.d.ts +3 -3
- package/dist/nav/workspace.js +1 -1
- package/dist/pipeline/figma.d.ts +13 -2
- package/dist/pipeline/figma.js +86 -46
- package/dist/pipeline/styles.js +1 -1
- package/dist/pipeline/validation.d.ts +1 -1
- package/dist/pipeline/validation.js +3 -3
- package/dist/providers.d.ts +10 -0
- package/dist/providers.js +13 -0
- package/dist/registry/asset-storage/define.d.ts +5 -5
- package/dist/registry/asset-storage/define.js +4 -4
- package/dist/registry/asset-storage/resolve.d.ts +9 -19
- package/dist/registry/asset-storage/resolve.js +33 -21
- package/dist/registry/asset-storage/types.d.ts +37 -17
- package/dist/registry/asset-storage/types.js +5 -5
- package/dist/registry/auth/ai-keys.d.ts +32 -0
- package/dist/registry/auth/ai-keys.js +69 -0
- package/dist/registry/auth/crypto.d.ts +17 -0
- package/dist/registry/auth/crypto.js +61 -1
- package/dist/registry/auth/index.d.ts +2 -1
- package/dist/registry/auth/index.js +11 -2
- package/dist/registry/checkout/index.d.ts +2 -1
- package/dist/registry/checkout/index.js +57 -127
- package/dist/registry/client.js +53 -17
- package/dist/registry/db/driver.js +3 -3
- package/dist/registry/db/schema.d.ts +261 -3
- package/dist/registry/db/schema.js +29 -4
- package/dist/registry/email/define.d.ts +10 -0
- package/dist/registry/email/define.js +13 -0
- package/dist/registry/email/providers/smtp.d.ts +10 -0
- package/dist/registry/email/providers/smtp.js +73 -0
- package/dist/registry/email/resolve.d.ts +28 -0
- package/dist/registry/email/resolve.js +74 -0
- package/dist/registry/email/types.d.ts +24 -0
- package/dist/registry/email/types.js +6 -0
- package/dist/registry/path.d.ts +2 -0
- package/dist/registry/path.js +11 -1
- package/dist/registry/publish/package.js +8 -0
- package/dist/registry/transfer.d.ts +4 -0
- package/dist/store/registry.d.ts +3 -3
- package/dist/store/registry.js +4 -4
- package/dist/transformers/plugins/ssr-render.js +40 -15
- package/dist/transformers/preview/component/builder.d.ts +1 -9
- package/dist/transformers/preview/component/builder.js +17 -17
- package/dist/transformers/preview/component/css.d.ts +3 -1
- package/dist/transformers/preview/component/css.js +92 -51
- package/dist/transformers/preview/component/html.d.ts +3 -1
- package/dist/transformers/preview/component/html.js +6 -2
- package/dist/transformers/preview/component/javascript.d.ts +2 -1
- package/dist/transformers/preview/component/javascript.js +10 -4
- package/dist/transformers/preview/pattern/html.js +3 -1
- package/dist/transformers/preview/types.d.ts +13 -0
- package/dist/transformers/utils/build.d.ts +24 -0
- package/dist/transformers/utils/build.js +100 -1
- package/dist/transformers/utils/module.js +2 -1
- package/dist/types/config.d.ts +135 -18
- package/dist/utils/path.d.ts +1 -5
- package/dist/utils/path.js +6 -23
- package/docs/api.md +6 -4
- package/docs/cli.md +275 -54
- package/drizzle/0007_add_ai_keys.sql +11 -0
- package/drizzle/0008_add_source_dir.sql +2 -0
- package/drizzle/meta/0007_snapshot.json +2013 -0
- package/drizzle/meta/0008_snapshot.json +2025 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +19 -8
- package/src/ai/README.md +42 -0
- package/src/ai/connections.ts +189 -0
- package/src/ai/define.ts +15 -0
- package/src/ai/types.ts +30 -0
- package/src/app/components/AiAssistant/AiAssistantPanel.tsx +144 -0
- package/src/app/components/AiAssistant/AiAssistantProvider.tsx +215 -0
- package/src/app/components/AiAssistant/AiConversation.tsx +737 -0
- package/src/app/components/AiAssistant/AiToolResult.tsx +99 -0
- package/src/app/components/AiAssistant/dockState.ts +86 -0
- package/src/app/components/AiAssistant/toolLabel.ts +24 -0
- package/src/app/components/AiAssistant/useAiChat.ts +147 -0
- package/src/app/components/AiAssistant/useAiConnections.ts +86 -0
- package/src/app/components/AiAssistant/usePageContext.ts +63 -0
- package/src/app/components/Auth/AccountLayout.tsx +58 -19
- package/src/app/components/Auth/AuthShell.tsx +27 -33
- package/src/app/components/Auth/RegistryInstallGate.tsx +4 -8
- package/src/app/components/Auth/installStatus.ts +29 -0
- package/src/app/components/Component/PageSliceResolver.tsx +1 -0
- package/src/app/components/Component/Preview.tsx +383 -189
- package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +4 -0
- package/src/app/components/Foundations/ColorGrid.tsx +13 -14
- package/src/app/components/Layout/Header.tsx +21 -15
- package/src/app/components/Layout/Main.tsx +2 -2
- package/src/app/components/Markdown/MarkdownComponents.tsx +30 -32
- package/src/app/components/McpIntegration/McpConfigDialog.tsx +12 -51
- package/src/app/components/Navigation/MobileNav.tsx +2 -2
- package/src/app/components/Navigation/SideNav.tsx +1 -15
- package/src/app/components/context/NavProvider.tsx +7 -3
- package/src/app/components/ui/attachment.tsx +50 -0
- package/src/app/components/ui/bubble.tsx +57 -0
- package/src/app/components/ui/button.tsx +3 -1
- package/src/app/components/ui/confirm-dialog.tsx +87 -0
- package/src/app/components/ui/dialog.tsx +7 -2
- package/src/app/components/ui/marker.tsx +56 -0
- package/src/app/components/ui/sidebar.tsx +3 -1
- package/src/app/components/util/index.ts +3 -12
- package/src/app/css/index.css +64 -0
- package/src/app/lib/ai/auth.ts +33 -0
- package/src/app/lib/ai/context.ts +99 -0
- package/src/app/lib/ai/model.ts +79 -0
- package/src/app/lib/ai/prompt.ts +36 -0
- package/src/app/lib/ai/resolve.ts +97 -0
- package/src/app/lib/ai/tools.ts +120 -0
- package/src/app/lib/api/methods.ts +13 -0
- package/src/app/lib/asset-storage.ts +35 -36
- package/src/app/lib/auth/api.ts +4 -12
- package/src/app/lib/auth/email.ts +96 -23
- package/src/app/lib/docs-api/page-rendering.ts +1 -1
- package/src/app/lib/docs-api/registry.ts +2 -3
- package/src/app/lib/docs-api/runtime-config.ts +47 -21
- package/src/app/lib/mcp/components.ts +523 -0
- package/src/app/lib/mcp/page-url.ts +24 -0
- package/src/app/lib/mcp/pages.ts +84 -0
- package/src/app/lib/mcp/result.ts +33 -0
- package/src/app/lib/mcp/server.ts +39 -318
- package/src/app/lib/mcp/tokens.ts +334 -0
- package/src/app/lib/mcp/tool.ts +19 -0
- package/src/app/lib/registry-api/asset-transfer.ts +119 -11
- package/src/app/lib/registry-api/errors.ts +2 -0
- package/src/app/lib/registry-api/handler.ts +7 -1
- package/src/app/lib/registry-api/transfer.ts +28 -6
- package/src/app/lib/server-module.ts +34 -0
- package/src/app/lib/utils.ts +20 -0
- package/src/app/next.config.mjs +26 -24
- package/src/app/pages/[...slug]/index.tsx +2 -1
- package/src/app/pages/_app.tsx +6 -2
- package/src/app/pages/_document.tsx +6 -0
- package/src/app/pages/account/ai.tsx +183 -0
- package/src/app/pages/account/index.tsx +68 -68
- package/src/app/pages/account/tokens.tsx +37 -7
- package/src/app/pages/account/users.tsx +75 -15
- package/src/app/pages/api/account/ai/keys/[connectionId].ts +60 -0
- package/src/app/pages/api/account/ai/keys/index.ts +20 -0
- package/src/app/pages/api/admin/users/[id]/[action].ts +80 -11
- package/src/app/pages/api/admin/users/index.ts +2 -2
- package/src/app/pages/api/ai/chat.ts +115 -0
- package/src/app/pages/api/ai/connections.ts +23 -0
- package/src/app/pages/api/auth/request-reset.ts +14 -16
- package/src/app/pages/api/docs/nav.json.ts +1 -1
- package/src/app/pages/api/install.ts +3 -3
- package/src/app/pages/api/mcp.ts +6 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/complete.ts +11 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/upload.ts +12 -0
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +1 -1
- package/src/app/pages/cli/device.tsx +10 -5
- package/src/app/pages/foundations/colors.tsx +9 -8
- package/src/app/pages/foundations/index.tsx +3 -3
- package/src/app/pages/index.tsx +2 -2
- package/src/app/pages/install.tsx +7 -2
- package/src/app/pages/login.tsx +32 -27
- package/src/app/pages/reset-password.tsx +21 -20
- package/src/app/pages/system/component/[component]/index.tsx +2 -5
- package/src/app/pages/system/index.tsx +11 -11
- package/src/app/pages/system/pattern/[pattern]/index.tsx +2 -2
- package/src/app/pages/system/tokens/components/[component]/index.tsx +0 -4
- package/src/app-builder/README.md +2 -1
- package/src/app-builder/build.ts +218 -73
- package/src/app-builder/client-config.ts +3 -2
- package/src/app-builder/paths.ts +3 -22
- package/src/app-builder/static-export.ts +1 -1
- package/src/app-builder/vercel-output.ts +5 -4
- package/src/cache/README.md +4 -1
- package/src/cache/build-cache.ts +128 -43
- package/src/cache/build-inputs.ts +28 -0
- package/src/cache/index.ts +4 -0
- package/src/catalog/README.md +3 -0
- package/src/catalog/normalize.ts +2 -0
- package/src/catalog/previews.ts +6 -3
- package/src/catalog/types.ts +8 -0
- package/src/cli/create.ts +1 -2
- package/src/commands/build/index.ts +1 -1
- package/src/config/README.md +10 -9
- package/src/config/defaults.ts +7 -4
- package/src/config/from-env.ts +33 -0
- package/src/config/helpers.ts +1 -3
- package/src/config/index.ts +0 -3
- package/src/config/loader.ts +27 -3
- package/src/config/resolve-env.ts +28 -15
- package/src/config/runtime.ts +10 -5
- package/src/handlebars.ts +8 -2
- package/src/index.ts +33 -29
- package/src/nav/index.ts +2 -2
- package/src/nav/registry.ts +3 -3
- package/src/nav/workspace.ts +3 -3
- package/src/pipeline/README.md +1 -1
- package/src/pipeline/figma.ts +98 -49
- package/src/pipeline/styles.ts +1 -1
- package/src/pipeline/validation.ts +3 -3
- package/src/providers.ts +18 -0
- package/src/registry/asset-storage/define.ts +6 -6
- package/src/registry/asset-storage/resolve.ts +38 -37
- package/src/registry/asset-storage/types.ts +39 -17
- package/src/registry/auth/ai-keys.ts +62 -0
- package/src/registry/auth/crypto.ts +63 -1
- package/src/registry/auth/index.ts +11 -1
- package/src/registry/checkout/index.ts +60 -138
- package/src/registry/client.ts +60 -20
- package/src/registry/db/driver.ts +2 -2
- package/src/registry/db/schema.ts +49 -22
- package/src/registry/email/define.ts +15 -0
- package/src/registry/email/providers/smtp.ts +39 -0
- package/src/registry/email/resolve.ts +94 -0
- package/src/registry/email/types.ts +28 -0
- package/src/registry/path.ts +11 -0
- package/src/registry/publish/package.ts +9 -0
- package/src/registry/transfer.ts +4 -0
- package/src/store/registry.ts +7 -7
- package/src/transformers/plugins/ssr-render.ts +53 -16
- package/src/transformers/preview/component/builder.ts +19 -25
- package/src/transformers/preview/component/css.ts +101 -53
- package/src/transformers/preview/component/html.ts +6 -2
- package/src/transformers/preview/component/javascript.ts +14 -4
- package/src/transformers/preview/pattern/html.ts +3 -1
- package/src/transformers/preview/types.ts +14 -0
- package/src/transformers/utils/build.ts +93 -0
- package/src/transformers/utils/module.ts +4 -2
- package/src/types/config.ts +148 -20
- package/src/utils/README.md +1 -1
- package/src/utils/path.ts +6 -29
- package/templates/default/handoff.config.js.tpl +10 -2
- package/templates/default/handoff.config.ts.tpl +10 -2
- package/templates/default/handoff.config.with-examples.js.tpl +10 -2
- package/templates/default/handoff.config.with-examples.ts.tpl +10 -2
- package/templates/default/package.json.tpl +2 -2
- package/.editorconfig +0 -19
- package/.eslintrc.json +0 -4
- package/.prettierignore +0 -0
- package/.prettierrc.json +0 -8
- package/.vscode/settings.json +0 -17
- package/dist/config/validator.d.ts +0 -9
- package/dist/config/validator.js +0 -24
- package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +0 -17
- package/dist/registry/asset-storage/adapters/vercel-blob.js +0 -87
- package/src/app/components/old/Header.tsx +0 -89
- package/src/app/lib/mcp/shape.ts +0 -596
- package/src/app/pages/assets/fonts.tsx +0 -129
- package/src/app/pages/assets/index.tsx +0 -91
- package/src/app/pages/assets/logos.tsx +0 -75
- package/src/config/validator.ts +0 -22
- package/src/registry/asset-storage/adapters/vercel-blob.ts +0 -51
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.readRegistryAiKey = exports.deleteRegistryAiKey = exports.saveRegistryAiKey = exports.listRegistryAiKeyConnections = void 0;
|
|
13
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
14
|
+
const schema_1 = require("../db/schema");
|
|
15
|
+
const crypto_1 = require("./crypto");
|
|
16
|
+
/**
|
|
17
|
+
* Reader-supplied keys for connections the config declares with `credential: 'user'`.
|
|
18
|
+
*
|
|
19
|
+
* The key is write-only across the API boundary: once saved, a read reports only that a key exists.
|
|
20
|
+
* Only the chat route ever decrypts one, and only to send it to the connection's own endpoint.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Ids of the connections this reader has a *usable* key for. A row that no longer decrypts — written
|
|
24
|
+
* under a rotated `HANDOFF_AI_KEY_SECRET` — is left out, so the account page and the assistant agree
|
|
25
|
+
* that no key is in place instead of offering a key the provider would never receive.
|
|
26
|
+
*/
|
|
27
|
+
const listRegistryAiKeyConnections = (db, userId) => __awaiter(void 0, void 0, void 0, function* () {
|
|
28
|
+
const rows = yield db
|
|
29
|
+
.select({ connectionId: schema_1.registryAiKeys.connectionId, encryptedKey: schema_1.registryAiKeys.encryptedKey })
|
|
30
|
+
.from(schema_1.registryAiKeys)
|
|
31
|
+
.where((0, drizzle_orm_1.eq)(schema_1.registryAiKeys.userId, userId))
|
|
32
|
+
.orderBy(schema_1.registryAiKeys.connectionId);
|
|
33
|
+
return rows.filter((row) => (0, crypto_1.decryptSecret)(row.encryptedKey) !== null).map((row) => row.connectionId);
|
|
34
|
+
});
|
|
35
|
+
exports.listRegistryAiKeyConnections = listRegistryAiKeyConnections;
|
|
36
|
+
/** Save or replace this reader's key for one connection. */
|
|
37
|
+
const saveRegistryAiKey = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
|
|
38
|
+
const encryptedKey = (0, crypto_1.encryptSecret)(input.key);
|
|
39
|
+
yield db
|
|
40
|
+
.insert(schema_1.registryAiKeys)
|
|
41
|
+
.values({ userId: input.userId, connectionId: input.connectionId, encryptedKey })
|
|
42
|
+
.onConflictDoUpdate({
|
|
43
|
+
target: [schema_1.registryAiKeys.userId, schema_1.registryAiKeys.connectionId],
|
|
44
|
+
set: { encryptedKey, updatedAt: new Date() },
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
exports.saveRegistryAiKey = saveRegistryAiKey;
|
|
48
|
+
/** Remove this reader's key for one connection. Returns whether a row was there to remove. */
|
|
49
|
+
const deleteRegistryAiKey = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
|
|
50
|
+
const rows = yield db
|
|
51
|
+
.delete(schema_1.registryAiKeys)
|
|
52
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryAiKeys.userId, input.userId), (0, drizzle_orm_1.eq)(schema_1.registryAiKeys.connectionId, input.connectionId)))
|
|
53
|
+
.returning({ connectionId: schema_1.registryAiKeys.connectionId });
|
|
54
|
+
return rows.length > 0;
|
|
55
|
+
});
|
|
56
|
+
exports.deleteRegistryAiKey = deleteRegistryAiKey;
|
|
57
|
+
/**
|
|
58
|
+
* Read back one reader's key. A row that no longer decrypts reads as absent, so the reader is told
|
|
59
|
+
* to add a key rather than shown a provider error.
|
|
60
|
+
*/
|
|
61
|
+
const readRegistryAiKey = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
|
|
62
|
+
const [row] = yield db
|
|
63
|
+
.select({ encryptedKey: schema_1.registryAiKeys.encryptedKey })
|
|
64
|
+
.from(schema_1.registryAiKeys)
|
|
65
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryAiKeys.userId, input.userId), (0, drizzle_orm_1.eq)(schema_1.registryAiKeys.connectionId, input.connectionId)))
|
|
66
|
+
.limit(1);
|
|
67
|
+
return row ? (0, crypto_1.decryptSecret)(row.encryptedKey) : null;
|
|
68
|
+
});
|
|
69
|
+
exports.readRegistryAiKey = readRegistryAiKey;
|
|
@@ -2,3 +2,20 @@ export declare const hashSecret: (secret: string) => string;
|
|
|
2
2
|
export declare const createOpaqueSecret: (byteLength?: number) => string;
|
|
3
3
|
export declare const secretHashMatches: (secret: string, expectedHash: string) => boolean;
|
|
4
4
|
export declare const normalizeEmail: (email: string) => string;
|
|
5
|
+
/** Env var holding the deployment secret that reader-supplied AI keys are encrypted with. */
|
|
6
|
+
export declare const AI_KEY_SECRET_ENV = "HANDOFF_AI_KEY_SECRET";
|
|
7
|
+
/** Thrown when a key must be encrypted but the deployment secret is missing or too short. */
|
|
8
|
+
export declare class MissingEncryptionSecretError extends Error {
|
|
9
|
+
constructor();
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Encrypt a value that has to be read back, such as a reader's provider key. AES-256-GCM, with the
|
|
13
|
+
* salt, nonce and authentication tag carried in the payload so a row is self-describing.
|
|
14
|
+
*/
|
|
15
|
+
export declare const encryptSecret: (plaintext: string) => string;
|
|
16
|
+
/**
|
|
17
|
+
* Decrypt a value written by {@link encryptSecret}. Returns `null` for anything that does not
|
|
18
|
+
* authenticate — a payload in another format, or one written under a secret that has since been
|
|
19
|
+
* rotated — so a stale row reads as "no key saved" instead of failing the request.
|
|
20
|
+
*/
|
|
21
|
+
export declare const decryptSecret: (payload: string) => string | null;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizeEmail = exports.secretHashMatches = exports.createOpaqueSecret = exports.hashSecret = void 0;
|
|
3
|
+
exports.decryptSecret = exports.encryptSecret = exports.MissingEncryptionSecretError = exports.AI_KEY_SECRET_ENV = exports.normalizeEmail = exports.secretHashMatches = exports.createOpaqueSecret = exports.hashSecret = void 0;
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
const hashSecret = (secret) => (0, node_crypto_1.createHash)('sha256').update(secret, 'utf8').digest('hex');
|
|
6
6
|
exports.hashSecret = hashSecret;
|
|
@@ -14,3 +14,63 @@ const secretHashMatches = (secret, expectedHash) => {
|
|
|
14
14
|
exports.secretHashMatches = secretHashMatches;
|
|
15
15
|
const normalizeEmail = (email) => email.trim().toLowerCase();
|
|
16
16
|
exports.normalizeEmail = normalizeEmail;
|
|
17
|
+
/** Env var holding the deployment secret that reader-supplied AI keys are encrypted with. */
|
|
18
|
+
exports.AI_KEY_SECRET_ENV = 'HANDOFF_AI_KEY_SECRET';
|
|
19
|
+
/** Shortest secret worth accepting. Matches the guidance the installer gives for `AUTH_SECRET`. */
|
|
20
|
+
const MIN_KEY_SECRET_LENGTH = 32;
|
|
21
|
+
/** Format marker, so a later algorithm is distinguishable from this one rather than guessed at. */
|
|
22
|
+
const ENCRYPTED_SECRET_VERSION = 'v1';
|
|
23
|
+
/** Thrown when a key must be encrypted but the deployment secret is missing or too short. */
|
|
24
|
+
class MissingEncryptionSecretError extends Error {
|
|
25
|
+
constructor() {
|
|
26
|
+
super(`Reader-supplied AI keys cannot be stored because ${exports.AI_KEY_SECRET_ENV} is not set. Set it to a ` +
|
|
27
|
+
`random value of at least ${MIN_KEY_SECRET_LENGTH} characters, or declare no connection with ` +
|
|
28
|
+
`"credential: 'user'".`);
|
|
29
|
+
this.name = 'MissingEncryptionSecretError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
exports.MissingEncryptionSecretError = MissingEncryptionSecretError;
|
|
33
|
+
/**
|
|
34
|
+
* Derive the record key. A per-record salt means two rows holding the same provider key produce
|
|
35
|
+
* different ciphertexts, and the fixed `info` label keeps this key distinct from anything else the
|
|
36
|
+
* same secret is ever used for.
|
|
37
|
+
*/
|
|
38
|
+
const deriveKey = (salt) => {
|
|
39
|
+
var _a;
|
|
40
|
+
const secret = (_a = process.env[exports.AI_KEY_SECRET_ENV]) === null || _a === void 0 ? void 0 : _a.trim();
|
|
41
|
+
if (!secret || secret.length < MIN_KEY_SECRET_LENGTH)
|
|
42
|
+
throw new MissingEncryptionSecretError();
|
|
43
|
+
return Buffer.from((0, node_crypto_1.hkdfSync)('sha256', Buffer.from(secret, 'utf8'), salt, 'handoff-ai-key', 32));
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Encrypt a value that has to be read back, such as a reader's provider key. AES-256-GCM, with the
|
|
47
|
+
* salt, nonce and authentication tag carried in the payload so a row is self-describing.
|
|
48
|
+
*/
|
|
49
|
+
const encryptSecret = (plaintext) => {
|
|
50
|
+
const salt = (0, node_crypto_1.randomBytes)(16);
|
|
51
|
+
const iv = (0, node_crypto_1.randomBytes)(12);
|
|
52
|
+
const cipher = (0, node_crypto_1.createCipheriv)('aes-256-gcm', deriveKey(salt), iv);
|
|
53
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
54
|
+
const parts = [salt, iv, cipher.getAuthTag(), encrypted].map((part) => part.toString('base64url'));
|
|
55
|
+
return [ENCRYPTED_SECRET_VERSION, ...parts].join('.');
|
|
56
|
+
};
|
|
57
|
+
exports.encryptSecret = encryptSecret;
|
|
58
|
+
/**
|
|
59
|
+
* Decrypt a value written by {@link encryptSecret}. Returns `null` for anything that does not
|
|
60
|
+
* authenticate — a payload in another format, or one written under a secret that has since been
|
|
61
|
+
* rotated — so a stale row reads as "no key saved" instead of failing the request.
|
|
62
|
+
*/
|
|
63
|
+
const decryptSecret = (payload) => {
|
|
64
|
+
const [version, salt, iv, tag, encrypted] = payload.split('.');
|
|
65
|
+
if (version !== ENCRYPTED_SECRET_VERSION || !salt || !iv || !tag || !encrypted)
|
|
66
|
+
return null;
|
|
67
|
+
try {
|
|
68
|
+
const decipher = (0, node_crypto_1.createDecipheriv)('aes-256-gcm', deriveKey(Buffer.from(salt, 'base64url')), Buffer.from(iv, 'base64url'));
|
|
69
|
+
decipher.setAuthTag(Buffer.from(tag, 'base64url'));
|
|
70
|
+
return Buffer.concat([decipher.update(Buffer.from(encrypted, 'base64url')), decipher.final()]).toString('utf8');
|
|
71
|
+
}
|
|
72
|
+
catch (_a) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
exports.decryptSecret = decryptSecret;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { createRegistryAccessToken, listRegistryAccessTokens, revokeRegistryAccessToken, revokeAllRegistryAccessTokens, authenticateRegistryAccessToken, formatRegistryAccessToken, parseRegistryAccessToken, normalizeRegistryScopes, expandRegistryScopes, resolveRegistryScope, registryRoleAllowsScopes, type RegistryAccessTokenSummary, type CreateRegistryAccessTokenResult, } from './access-tokens';
|
|
2
2
|
export { createUserInvitation, resendUserInvitation, acceptUserInvitation, createPasswordReset, resetPassword, INVITATION_LIFETIME_MS, PASSWORD_RESET_LIFETIME_MS, type CreateUserInvitationResult, type ResendUserInvitationResult, type ConsumeAuthActionResult, } from './action-tokens';
|
|
3
|
-
export { createOpaqueSecret, hashSecret, normalizeEmail, secretHashMatches } from './crypto';
|
|
3
|
+
export { AI_KEY_SECRET_ENV, MissingEncryptionSecretError, createOpaqueSecret, decryptSecret, encryptSecret, hashSecret, normalizeEmail, secretHashMatches, } from './crypto';
|
|
4
|
+
export { deleteRegistryAiKey, listRegistryAiKeyConnections, readRegistryAiKey, saveRegistryAiKey } from './ai-keys';
|
|
4
5
|
export { buildGravatarUrl } from './gravatar';
|
|
5
6
|
export { authenticateRegistrySyncSecret, resolveRegistrySyncSecret, SYNC_SECRET_ENV } from './sync-secret';
|
|
6
7
|
export { createRegistryDeviceAuthorization, approveRegistryDeviceAuthorization, denyRegistryDeviceAuthorization, exchangeRegistryDeviceAuthorization, purgeExpiredRegistryDeviceAuthorizations, generateRegistryUserCode, normalizeRegistryUserCode, DEVICE_AUTHORIZATION_LIFETIME_SECONDS, DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS, type CreateRegistryDeviceAuthorizationResult, type RegistryDeviceApprovalResult, type ExchangeRegistryDeviceAuthorizationResult, } from './device-authorizations';
|
|
@@ -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.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;
|
|
3
|
+
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.saveRegistryAiKey = exports.readRegistryAiKey = exports.listRegistryAiKeyConnections = exports.deleteRegistryAiKey = exports.secretHashMatches = exports.normalizeEmail = exports.hashSecret = exports.encryptSecret = exports.decryptSecret = exports.createOpaqueSecret = exports.MissingEncryptionSecretError = exports.AI_KEY_SECRET_ENV = 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 = exports.getRegistryUserById = exports.hashRateLimitIdentifier = exports.purgeExpiredAuthRateLimits = exports.clearAuthRateLimit = exports.consumeAuthRateLimit = exports.MAXIMUM_PASSWORD_LENGTH = exports.MINIMUM_PASSWORD_LENGTH = exports.validatePassword = 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; } });
|
|
@@ -23,10 +23,19 @@ Object.defineProperty(exports, "resetPassword", { enumerable: true, get: functio
|
|
|
23
23
|
Object.defineProperty(exports, "INVITATION_LIFETIME_MS", { enumerable: true, get: function () { return action_tokens_1.INVITATION_LIFETIME_MS; } });
|
|
24
24
|
Object.defineProperty(exports, "PASSWORD_RESET_LIFETIME_MS", { enumerable: true, get: function () { return action_tokens_1.PASSWORD_RESET_LIFETIME_MS; } });
|
|
25
25
|
var crypto_1 = require("./crypto");
|
|
26
|
+
Object.defineProperty(exports, "AI_KEY_SECRET_ENV", { enumerable: true, get: function () { return crypto_1.AI_KEY_SECRET_ENV; } });
|
|
27
|
+
Object.defineProperty(exports, "MissingEncryptionSecretError", { enumerable: true, get: function () { return crypto_1.MissingEncryptionSecretError; } });
|
|
26
28
|
Object.defineProperty(exports, "createOpaqueSecret", { enumerable: true, get: function () { return crypto_1.createOpaqueSecret; } });
|
|
29
|
+
Object.defineProperty(exports, "decryptSecret", { enumerable: true, get: function () { return crypto_1.decryptSecret; } });
|
|
30
|
+
Object.defineProperty(exports, "encryptSecret", { enumerable: true, get: function () { return crypto_1.encryptSecret; } });
|
|
27
31
|
Object.defineProperty(exports, "hashSecret", { enumerable: true, get: function () { return crypto_1.hashSecret; } });
|
|
28
32
|
Object.defineProperty(exports, "normalizeEmail", { enumerable: true, get: function () { return crypto_1.normalizeEmail; } });
|
|
29
33
|
Object.defineProperty(exports, "secretHashMatches", { enumerable: true, get: function () { return crypto_1.secretHashMatches; } });
|
|
34
|
+
var ai_keys_1 = require("./ai-keys");
|
|
35
|
+
Object.defineProperty(exports, "deleteRegistryAiKey", { enumerable: true, get: function () { return ai_keys_1.deleteRegistryAiKey; } });
|
|
36
|
+
Object.defineProperty(exports, "listRegistryAiKeyConnections", { enumerable: true, get: function () { return ai_keys_1.listRegistryAiKeyConnections; } });
|
|
37
|
+
Object.defineProperty(exports, "readRegistryAiKey", { enumerable: true, get: function () { return ai_keys_1.readRegistryAiKey; } });
|
|
38
|
+
Object.defineProperty(exports, "saveRegistryAiKey", { enumerable: true, get: function () { return ai_keys_1.saveRegistryAiKey; } });
|
|
30
39
|
var gravatar_1 = require("./gravatar");
|
|
31
40
|
Object.defineProperty(exports, "buildGravatarUrl", { enumerable: true, get: function () { return gravatar_1.buildGravatarUrl; } });
|
|
32
41
|
var sync_secret_1 = require("./sync-secret");
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* into the local workspace in standard authoring form. The local **declaration is synthesized
|
|
7
7
|
* locally** in the configured `runtime.workspace.declarationFormat` — declarations are never read
|
|
8
8
|
* from the registry (they are workspace-only). Identity is matched by stable `id`, so a checked-out
|
|
9
|
-
* entity maps to the correct logical component/pattern and can be re-authored and re-published.
|
|
9
|
+
* entity maps to the correct logical component/pattern and can be re-authored and re-published. A
|
|
10
|
+
* new entity goes to the directory it was published from (`sourceDir`).
|
|
10
11
|
*
|
|
11
12
|
* Available only from a connected workspace (`runtime.mode: workspace` + a configured
|
|
12
13
|
* `registryConnection`). Overwriting existing local files is explicit: a `--force` flag or an
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* into the local workspace in standard authoring form. The local **declaration is synthesized
|
|
8
8
|
* locally** in the configured `runtime.workspace.declarationFormat` — declarations are never read
|
|
9
9
|
* from the registry (they are workspace-only). Identity is matched by stable `id`, so a checked-out
|
|
10
|
-
* entity maps to the correct logical component/pattern and can be re-authored and re-published.
|
|
10
|
+
* entity maps to the correct logical component/pattern and can be re-authored and re-published. A
|
|
11
|
+
* new entity goes to the directory it was published from (`sourceDir`).
|
|
11
12
|
*
|
|
12
13
|
* Available only from a connected workspace (`runtime.mode: workspace` + a configured
|
|
13
14
|
* `registryConnection`). Overwriting existing local files is explicit: a `--force` flag or an
|
|
@@ -69,12 +70,12 @@ const path_1 = __importDefault(require("path"));
|
|
|
69
70
|
const previews_1 = require("../../catalog/previews");
|
|
70
71
|
const define_1 = require("../../catalog/define");
|
|
71
72
|
const renderers_1 = require("../../catalog/renderers");
|
|
72
|
-
const catalog_include_1 = require("../../config/catalog-include");
|
|
73
73
|
const runtime_1 = require("../../config/runtime");
|
|
74
74
|
const logger_1 = require("../../utils/logger");
|
|
75
|
+
const path_2 = require("../../utils/path");
|
|
75
76
|
const client_1 = require("../client");
|
|
76
77
|
const connection_1 = require("../connection");
|
|
77
|
-
const
|
|
78
|
+
const path_3 = require("../path");
|
|
78
79
|
const selection_1 = require("../selection");
|
|
79
80
|
/** A connected-workspace configuration or precondition failure surfaced to the CLI. */
|
|
80
81
|
class CheckoutError extends Error {
|
|
@@ -86,12 +87,7 @@ class CheckoutError extends Error {
|
|
|
86
87
|
exports.CheckoutError = CheckoutError;
|
|
87
88
|
/** Default authoring format synthesized when no existing file or configured preference applies. */
|
|
88
89
|
const DEFAULT_DECLARATION_FORMAT = 'ts';
|
|
89
|
-
|
|
90
|
-
const DEFAULT_ENTITY_DIR = {
|
|
91
|
-
component: 'components',
|
|
92
|
-
pattern: 'patterns',
|
|
93
|
-
page: 'pages',
|
|
94
|
-
};
|
|
90
|
+
const PAGES_DIR = 'pages';
|
|
95
91
|
/** Source-file extensions stripped to form a module specifier for a React component import. */
|
|
96
92
|
const COMPONENT_EXTENSION = /\.(tsx|jsx|ts|js|cjs|mjs)$/i;
|
|
97
93
|
/**
|
|
@@ -133,78 +129,40 @@ const describeFetchFailure = (error, kind, id, registryUrl) => {
|
|
|
133
129
|
return error.message;
|
|
134
130
|
}
|
|
135
131
|
};
|
|
136
|
-
/**
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const relative = (root) => path_1.default.relative(handoff.workingPath, root) || '.';
|
|
143
|
-
if (handoff.force) {
|
|
144
|
-
throw new CheckoutError(`Cannot determine where to checkout the ${kind}: catalog.include declares directories under different ` +
|
|
145
|
-
`parents (${roots.map(relative).join(', ')}), and none is named "${DEFAULT_ENTITY_DIR[kind]}". Declare a single ` +
|
|
146
|
-
`collection directory in handoff.config, or run checkout without --force to choose interactively.`);
|
|
147
|
-
}
|
|
148
|
-
const choice = yield p.select({
|
|
149
|
-
message: `Where should the new ${kind} be checked out?`,
|
|
150
|
-
options: roots.map((root) => ({ value: root, label: relative(root) })),
|
|
151
|
-
initialValue: roots[0],
|
|
132
|
+
/** `dir` is a `catalog.include` entry, or a direct child of a collection entry. */
|
|
133
|
+
const isInCatalog = (handoff, dir) => {
|
|
134
|
+
var _a, _b, _c;
|
|
135
|
+
return ((_c = (_b = (_a = handoff.config) === null || _a === void 0 ? void 0 : _a.catalog) === null || _b === void 0 ? void 0 : _b.include) !== null && _c !== void 0 ? _c : []).some((entry) => {
|
|
136
|
+
const included = path_1.default.resolve(handoff.workingPath, entry);
|
|
137
|
+
return (0, path_2.arePathsEqual)(included, dir) || (!(0, runtime_1.isComponentDirectory)(included) && (0, path_2.arePathsEqual)(included, path_1.default.dirname(dir)));
|
|
152
138
|
});
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return choice;
|
|
157
|
-
});
|
|
158
|
-
/**
|
|
159
|
-
* Resolve the collection directory a new entity is cloned into from the configured
|
|
160
|
-
* `catalog.include`. Each entry is either a collection directory (used as-is) or a
|
|
161
|
-
* single declared entity directory, in which case its parent is the collection root so the new
|
|
162
|
-
* entity lands as a sibling rather than nested inside. Different parents are settled by the
|
|
163
|
-
* conventional directory name for the kind, and only then by asking the user. With nothing
|
|
164
|
-
* configured we fall back to the default subdir.
|
|
165
|
-
*/
|
|
166
|
-
const resolveCollectionRoot = (handoff, kind) => __awaiter(void 0, void 0, void 0, function* () {
|
|
167
|
-
var _a, _b;
|
|
168
|
-
const configuredRoots = (_b = (_a = handoff.config) === null || _a === void 0 ? void 0 : _a.catalog) === null || _b === void 0 ? void 0 : _b.include;
|
|
169
|
-
if (!(configuredRoots === null || configuredRoots === void 0 ? void 0 : configuredRoots.length)) {
|
|
170
|
-
return path_1.default.resolve(handoff.workingPath, DEFAULT_ENTITY_DIR[kind]);
|
|
171
|
-
}
|
|
172
|
-
const roots = [
|
|
173
|
-
...new Set(configuredRoots.map((entry) => {
|
|
174
|
-
const resolved = path_1.default.resolve(handoff.workingPath, entry);
|
|
175
|
-
return (0, runtime_1.isComponentDirectory)(resolved) ? path_1.default.dirname(resolved) : resolved;
|
|
176
|
-
})),
|
|
177
|
-
];
|
|
178
|
-
if (roots.length === 1) {
|
|
179
|
-
return roots[0];
|
|
180
|
-
}
|
|
181
|
-
// `catalog.include` is one list for both lanes, so a project that registers `components` and
|
|
182
|
-
// `patterns` offers two roots for every checkout. Without this, every such project prompts, and a
|
|
183
|
-
// non-interactive `--force` run fails.
|
|
184
|
-
const conventional = roots.filter((root) => path_1.default.basename(root) === DEFAULT_ENTITY_DIR[kind]);
|
|
185
|
-
if (conventional.length === 1) {
|
|
186
|
-
return conventional[0];
|
|
187
|
-
}
|
|
188
|
-
return promptForCollectionRoot(handoff, kind, roots);
|
|
189
|
-
});
|
|
190
|
-
/**
|
|
191
|
-
* Resolve the local directory the entity is written into. An already-checked-out or locally-declared
|
|
192
|
-
* entity keeps its existing source directory (matched by stable `id`); otherwise it lands as a
|
|
193
|
-
* sibling under its configured collection root (see {@link resolveCollectionRoot}).
|
|
194
|
-
*/
|
|
195
|
-
const resolveTargetDir = (handoff, kind, id) => __awaiter(void 0, void 0, void 0, function* () {
|
|
139
|
+
};
|
|
140
|
+
/** A locally declared entity keeps its directory. A new one goes to the directory it was published from. */
|
|
141
|
+
const resolveTargetDir = (handoff, kind, id, sourceDir) => __awaiter(void 0, void 0, void 0, function* () {
|
|
196
142
|
const store = kind === 'component' ? handoff.store.components : handoff.store.patterns;
|
|
197
143
|
const existing = yield store.get(id);
|
|
198
144
|
const existingDir = existing === null || existing === void 0 ? void 0 : existing.path;
|
|
199
145
|
if (existingDir) {
|
|
200
|
-
return existingDir;
|
|
146
|
+
return { dir: existingDir, isInCatalog: true };
|
|
201
147
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (!targetDir) {
|
|
205
|
-
throw new CheckoutError(`Cannot checkout ${kind} with unsafe id "${id}".`);
|
|
148
|
+
if (!(0, path_3.isWorkspaceRelativePath)(sourceDir)) {
|
|
149
|
+
throw new CheckoutError(`The registry has no valid source directory for ${kind} "${id}". Republish it, then checkout again.`);
|
|
206
150
|
}
|
|
207
|
-
|
|
151
|
+
const dir = path_1.default.resolve(handoff.workingPath, sourceDir);
|
|
152
|
+
return { dir, isInCatalog: isInCatalog(handoff, dir) };
|
|
153
|
+
});
|
|
154
|
+
/** `--force` fails here: it skips overwrite prompts, not approval of a location the registry chose. */
|
|
155
|
+
const confirmLocation = (handoff, kind, id, dir) => __awaiter(void 0, void 0, void 0, function* () {
|
|
156
|
+
const relative = path_1.default.relative(handoff.workingPath, dir) || '.';
|
|
157
|
+
if (handoff.force) {
|
|
158
|
+
throw new CheckoutError(`The registry places ${kind} "${id}" at ${relative}, which is outside catalog.include. ` +
|
|
159
|
+
`Add "${relative}" or its parent directory to catalog.include, or run checkout without --force to accept the location.`);
|
|
160
|
+
}
|
|
161
|
+
const proceed = yield p.confirm({
|
|
162
|
+
message: `The registry places ${kind} "${id}" at ${relative}, which is outside catalog.include. Write it there?`,
|
|
163
|
+
initialValue: false,
|
|
164
|
+
});
|
|
165
|
+
return !p.isCancel(proceed) && proceed === true;
|
|
208
166
|
});
|
|
209
167
|
/** Read the extension of an existing local declaration in `dir`, if any maps to a known format. */
|
|
210
168
|
const existingDeclarationFormat = (dir) => {
|
|
@@ -685,7 +643,7 @@ const confirmOverwrite = (handoff, conflicts) => __awaiter(void 0, void 0, void
|
|
|
685
643
|
});
|
|
686
644
|
/** Write a registry source file at its relative path under the target directory. */
|
|
687
645
|
const writeSourceFile = (targetDir, file) => __awaiter(void 0, void 0, void 0, function* () {
|
|
688
|
-
const absolutePath = (0,
|
|
646
|
+
const absolutePath = (0, path_3.resolvePathWithin)(targetDir, file.path);
|
|
689
647
|
if (!absolutePath) {
|
|
690
648
|
throw new CheckoutError(`Registry returned an unsafe source file path "${file.path}".`);
|
|
691
649
|
}
|
|
@@ -714,9 +672,9 @@ exports.reportPlannedWrites = reportPlannedWrites;
|
|
|
714
672
|
* markdown file is itself the authored source — so the page round-trips byte-for-byte.
|
|
715
673
|
*/
|
|
716
674
|
const checkoutPage = (handoff, id, payload) => __awaiter(void 0, void 0, void 0, function* () {
|
|
717
|
-
const pagesRoot = path_1.default.resolve(handoff.workingPath,
|
|
675
|
+
const pagesRoot = path_1.default.resolve(handoff.workingPath, PAGES_DIR);
|
|
718
676
|
const targets = payload.files.map((file) => {
|
|
719
|
-
const target = (0,
|
|
677
|
+
const target = (0, path_3.resolvePathWithin)(pagesRoot, file.path);
|
|
720
678
|
if (!target) {
|
|
721
679
|
throw new CheckoutError(`Registry returned an unsafe page source path "${file.path}".`);
|
|
722
680
|
}
|
|
@@ -744,7 +702,7 @@ const checkoutPage = (handoff, id, payload) => __awaiter(void 0, void 0, void 0,
|
|
|
744
702
|
* failure.
|
|
745
703
|
*/
|
|
746
704
|
const checkoutSingle = (handoff, kind, id, client, registryUrl) => __awaiter(void 0, void 0, void 0, function* () {
|
|
747
|
-
const validId = kind === 'page' ? (0,
|
|
705
|
+
const validId = kind === 'page' ? (0, path_3.isSafeRelativePath)(id) : (0, path_3.isSafePathSegment)(id);
|
|
748
706
|
if (!validId) {
|
|
749
707
|
throw new CheckoutError(`Cannot checkout ${kind} with unsafe id "${id}".`);
|
|
750
708
|
}
|
|
@@ -759,19 +717,20 @@ const checkoutSingle = (handoff, kind, id, client, registryUrl) => __awaiter(voi
|
|
|
759
717
|
}
|
|
760
718
|
throw error;
|
|
761
719
|
}
|
|
762
|
-
// Pages round-trip as a single verbatim `.md` with no declaration synthesis, and aren't
|
|
763
|
-
// listed in `catalog.include`, so there's nothing to register.
|
|
764
720
|
if (kind === 'page') {
|
|
765
721
|
yield checkoutPage(handoff, id, payload);
|
|
766
|
-
return
|
|
722
|
+
return;
|
|
767
723
|
}
|
|
768
|
-
const
|
|
724
|
+
const target = yield resolveTargetDir(handoff, kind, id, payload.sourceDir);
|
|
725
|
+
const targetDir = target.dir;
|
|
726
|
+
const relativeDir = path_1.default.relative(handoff.workingPath, targetDir) || '.';
|
|
727
|
+
const outsideCatalogWarning = `Add "${relativeDir}" to catalog.include, or the build will not load ${kind} "${id}".`;
|
|
769
728
|
const format = resolveDeclarationFormat(handoff, targetDir);
|
|
770
729
|
const declarationFileName = resolveDeclarationFileName(targetDir, id, format);
|
|
771
730
|
const declarationPath = path_1.default.join(targetDir, declarationFileName);
|
|
772
731
|
// Compute everything that would be written, then gate overwrite of any pre-existing file.
|
|
773
732
|
const sourceTargets = payload.files.map((file) => {
|
|
774
|
-
const absolutePath = (0,
|
|
733
|
+
const absolutePath = (0, path_3.resolvePathWithin)(targetDir, file.path);
|
|
775
734
|
if (!absolutePath) {
|
|
776
735
|
throw new CheckoutError(`Registry returned an unsafe source file path "${file.path}".`);
|
|
777
736
|
}
|
|
@@ -781,13 +740,19 @@ const checkoutSingle = (handoff, kind, id, client, registryUrl) => __awaiter(voi
|
|
|
781
740
|
const plannedWrites = [...sourceTargets.map((entry) => entry.absolutePath), declarationPath];
|
|
782
741
|
const conflicts = plannedWrites.filter((file) => fs_extra_1.default.existsSync(file));
|
|
783
742
|
if (handoff.dryRun) {
|
|
784
|
-
// Returning null also skips `registerCheckedOut`, so a dry run never edits handoff.config either.
|
|
785
743
|
(0, exports.reportPlannedWrites)(handoff, `${kind} "${id}"`, plannedWrites, conflicts);
|
|
786
|
-
|
|
744
|
+
if (!target.isInCatalog) {
|
|
745
|
+
logger_1.Logger.warn(`${relativeDir} is outside catalog.include. ${outsideCatalogWarning}`);
|
|
746
|
+
}
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (!target.isInCatalog && !(yield confirmLocation(handoff, kind, id, targetDir))) {
|
|
750
|
+
logger_1.Logger.warn(`Checkout of ${kind} "${id}" cancelled; no files were written.`);
|
|
751
|
+
return;
|
|
787
752
|
}
|
|
788
753
|
if (!(yield confirmOverwrite(handoff, conflicts))) {
|
|
789
754
|
logger_1.Logger.warn(`Checkout of ${kind} "${id}" cancelled; no files were written.`);
|
|
790
|
-
return
|
|
755
|
+
return;
|
|
791
756
|
}
|
|
792
757
|
yield fs_extra_1.default.ensureDir(targetDir);
|
|
793
758
|
const written = [];
|
|
@@ -796,36 +761,10 @@ const checkoutSingle = (handoff, kind, id, client, registryUrl) => __awaiter(voi
|
|
|
796
761
|
}
|
|
797
762
|
yield fs_extra_1.default.writeFile(declarationPath, declaration, 'utf8');
|
|
798
763
|
written.push(declarationPath);
|
|
799
|
-
logger_1.Logger.success(`Checked out ${kind} "${id}" into ${
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
});
|
|
803
|
-
/**
|
|
804
|
-
* Declare freshly checked-out entities in `catalog.include` so the workspace build
|
|
805
|
-
* picks them up. Ones already covered by a collection directory load on their own and are left
|
|
806
|
-
* alone; the rest are added to the config automatically. If the config can't be edited (a computed
|
|
807
|
-
* or unusual `include` array), we print the paths for the user to add so nothing is silently orphaned.
|
|
808
|
-
*/
|
|
809
|
-
const registerCheckedOut = (handoff, kind, targetDirs) => __awaiter(void 0, void 0, void 0, function* () {
|
|
810
|
-
if (kind === 'page') {
|
|
811
|
-
return;
|
|
764
|
+
logger_1.Logger.success(`Checked out ${kind} "${id}" into ${relativeDir} (${payload.files.length} source file(s) + ${declarationFileName}).`);
|
|
765
|
+
if (!target.isInCatalog) {
|
|
766
|
+
logger_1.Logger.warn(outsideCatalogWarning);
|
|
812
767
|
}
|
|
813
|
-
const kindLabel = kind === 'component' ? 'components' : 'patterns';
|
|
814
|
-
const unlisted = targetDirs.filter((dir) => !(0, catalog_include_1.isIncluded)(handoff, dir));
|
|
815
|
-
if (unlisted.length === 0) {
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
|
-
const result = yield (0, catalog_include_1.addToCatalog)(handoff, unlisted);
|
|
819
|
-
if (result.status === 'added') {
|
|
820
|
-
const where = result.configPath
|
|
821
|
-
? path_1.default.relative(handoff.workingPath, result.configPath) || path_1.default.basename(result.configPath)
|
|
822
|
-
: 'handoff.config';
|
|
823
|
-
logger_1.Logger.success(`Updated ${where} with ${result.added.length} ${kindLabel} path(s).`);
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
|
-
const where = result.configPath ? path_1.default.relative(handoff.workingPath, result.configPath) : 'handoff.config';
|
|
827
|
-
logger_1.Logger.warn(`Could not update ${where} automatically. Add these to catalog.include manually:`);
|
|
828
|
-
result.pending.forEach((rel) => logger_1.Logger.warn(` - ${rel}`));
|
|
829
768
|
});
|
|
830
769
|
/**
|
|
831
770
|
* Checkout a single component, pattern, or page from the connected registry into this workspace.
|
|
@@ -834,10 +773,7 @@ const registerCheckedOut = (handoff, kind, targetDirs) => __awaiter(void 0, void
|
|
|
834
773
|
const checkoutEntity = (handoff, kind, id) => __awaiter(void 0, void 0, void 0, function* () {
|
|
835
774
|
const connection = yield (0, exports.resolveConnectionOrThrow)(handoff);
|
|
836
775
|
const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
|
|
837
|
-
|
|
838
|
-
if (targetDir) {
|
|
839
|
-
yield registerCheckedOut(handoff, kind, [targetDir]);
|
|
840
|
-
}
|
|
776
|
+
yield checkoutSingle(handoff, kind, id, client, connection.url);
|
|
841
777
|
});
|
|
842
778
|
exports.checkoutEntity = checkoutEntity;
|
|
843
779
|
/** List the ids the registry publishes for one kind, naming the registry in a listing failure. */
|
|
@@ -854,7 +790,7 @@ const listPublishedIds = (kind, client, registryUrl) => __awaiter(void 0, void 0
|
|
|
854
790
|
}
|
|
855
791
|
});
|
|
856
792
|
/**
|
|
857
|
-
* Check out the given ids of one kind
|
|
793
|
+
* Check out the given ids of one kind. The caller selects the ids, because a
|
|
858
794
|
* catalog run resolves them across both entity lanes before any of them is written.
|
|
859
795
|
*
|
|
860
796
|
* A per-entity failure is collected and never aborts the rest. Failures are reported here, and the
|
|
@@ -863,21 +799,15 @@ const listPublishedIds = (kind, client, registryUrl) => __awaiter(void 0, void 0
|
|
|
863
799
|
const checkoutSelected = (handoff, kind, targets, client, registryUrl) => __awaiter(void 0, void 0, void 0, function* () {
|
|
864
800
|
let checkedOut = 0;
|
|
865
801
|
const failed = [];
|
|
866
|
-
const targetDirs = [];
|
|
867
802
|
for (const id of targets) {
|
|
868
803
|
try {
|
|
869
|
-
|
|
870
|
-
if (targetDir) {
|
|
871
|
-
targetDirs.push(targetDir);
|
|
872
|
-
}
|
|
804
|
+
yield checkoutSingle(handoff, kind, id, client, registryUrl);
|
|
873
805
|
checkedOut += 1;
|
|
874
806
|
}
|
|
875
807
|
catch (error) {
|
|
876
808
|
failed.push({ id, message: error instanceof Error ? error.message : String(error) });
|
|
877
809
|
}
|
|
878
810
|
}
|
|
879
|
-
// Register everything checked out in one pass, so the config is edited (and confirmed) once.
|
|
880
|
-
yield registerCheckedOut(handoff, kind, targetDirs);
|
|
881
811
|
logger_1.Logger.success(`${kind[0].toUpperCase()}${kind.slice(1)}s checkout complete — ${checkedOut} ${handoff.dryRun ? 'would be checked out' : 'checked out'}` +
|
|
882
812
|
`${failed.length ? `, ${failed.length} failed` : ''}.`);
|
|
883
813
|
for (const failure of failed) {
|
package/dist/registry/client.js
CHANGED
|
@@ -100,6 +100,27 @@ const createRegistryClient = ({ baseUrl, accessToken }) => {
|
|
|
100
100
|
}
|
|
101
101
|
return envelope;
|
|
102
102
|
});
|
|
103
|
+
const uploadAssetBlobThroughRegistry = (blobPath, contentType, bytes) => __awaiter(void 0, void 0, void 0, function* () {
|
|
104
|
+
var _a, _b;
|
|
105
|
+
const url = `${root}${blobPath}`;
|
|
106
|
+
// Send a Blob, not a Buffer/Uint8Array: under Next `trailingSlash: true` this PUT is
|
|
107
|
+
// 308-redirected and undici must re-send the body; a typed-array body's ArrayBuffer is detached
|
|
108
|
+
// after the first send ("slice on a detached ArrayBuffer"), but a Blob is re-readable.
|
|
109
|
+
const body = new Blob([new Uint8Array(bytes)]);
|
|
110
|
+
const response = yield fetchRegistry(url, {
|
|
111
|
+
method: 'PUT',
|
|
112
|
+
headers: Object.assign({ 'Content-Type': contentType || 'application/octet-stream' }, (accessToken ? { Authorization: `Bearer ${accessToken}` } : {})),
|
|
113
|
+
body,
|
|
114
|
+
});
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
let code;
|
|
117
|
+
let message = `Blob upload failed (${response.status} ${response.statusText}).`;
|
|
118
|
+
const envelope = yield readErrorEnvelope(response);
|
|
119
|
+
code = (_a = envelope === null || envelope === void 0 ? void 0 : envelope.error) === null || _a === void 0 ? void 0 : _a.code;
|
|
120
|
+
message = ((_b = envelope === null || envelope === void 0 ? void 0 : envelope.error) === null || _b === void 0 ? void 0 : _b.message) || message;
|
|
121
|
+
throw new RegistryClientError(message, { status: response.status, code });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
103
124
|
return {
|
|
104
125
|
/** Publish (`PUT`) an entity's package to the registry transfer endpoint. */
|
|
105
126
|
publish(kind, id, pkg) {
|
|
@@ -161,28 +182,43 @@ const createRegistryClient = ({ baseUrl, accessToken }) => {
|
|
|
161
182
|
return (_b = (_a = envelope.data) === null || _a === void 0 ? void 0 : _a.missing) !== null && _b !== void 0 ? _b : [];
|
|
162
183
|
});
|
|
163
184
|
},
|
|
164
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* Upload one content-addressed blob. Sends it directly to storage when the registry returns a
|
|
187
|
+
* signed URL, otherwise through the registry. Idempotent: re-uploading an existing hash no-ops.
|
|
188
|
+
*/
|
|
165
189
|
uploadAssetBlob(hash, contentType, bytes) {
|
|
166
190
|
return __awaiter(this, void 0, void 0, function* () {
|
|
167
191
|
var _a, _b;
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
192
|
+
const blobPath = `/api/registry/transfer/assets/blobs/${encodeURIComponent(hash)}`;
|
|
193
|
+
const size = bytes.length;
|
|
194
|
+
const envelope = yield request('POST', `${blobPath}/upload`, { size, contentType });
|
|
195
|
+
if ((_a = envelope.data) === null || _a === void 0 ? void 0 : _a.stored) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const upload = (_b = envelope.data) === null || _b === void 0 ? void 0 : _b.upload;
|
|
199
|
+
if (!upload) {
|
|
200
|
+
yield uploadAssetBlobThroughRegistry(blobPath, contentType, bytes);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
// Name only the origin in errors: the signed query string must not reach logs.
|
|
204
|
+
const storage = new URL(upload.url).origin;
|
|
205
|
+
let response;
|
|
206
|
+
try {
|
|
207
|
+
response = yield fetch(upload.url, {
|
|
208
|
+
method: 'PUT',
|
|
209
|
+
headers: upload.headers,
|
|
210
|
+
body: new Blob([new Uint8Array(bytes)]),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
const cause = error === null || error === void 0 ? void 0 : error.cause;
|
|
215
|
+
throw new RegistryClientError(`Could not reach asset storage at ${storage}: ${(cause === null || cause === void 0 ? void 0 : cause.code) || (cause === null || cause === void 0 ? void 0 : cause.message) || 'network error'}.`);
|
|
216
|
+
}
|
|
178
217
|
if (!response.ok) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const envelope = yield readErrorEnvelope(response);
|
|
182
|
-
code = (_a = envelope === null || envelope === void 0 ? void 0 : envelope.error) === null || _a === void 0 ? void 0 : _a.code;
|
|
183
|
-
message = ((_b = envelope === null || envelope === void 0 ? void 0 : envelope.error) === null || _b === void 0 ? void 0 : _b.message) || message;
|
|
184
|
-
throw new RegistryClientError(message, { status: response.status, code });
|
|
218
|
+
const detail = (yield response.text().catch(() => '')).slice(0, 300);
|
|
219
|
+
throw new RegistryClientError(`Asset storage at ${storage} rejected the upload of blob "${hash}" (${response.status} ${response.statusText})${detail ? `: ${detail}` : ''}.`, { status: response.status });
|
|
185
220
|
}
|
|
221
|
+
yield request('POST', `${blobPath}/complete`, { storageRef: upload.storageRef, size, contentType });
|
|
186
222
|
});
|
|
187
223
|
},
|
|
188
224
|
/** Download one blob's raw bytes by hash (follows any provider redirect). */
|