handoff-app 2.0.0-2 → 2.0.0-4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +140 -83
  2. package/dist/app-builder/build.js +21 -4
  3. package/dist/app-builder/client-config.js +9 -8
  4. package/dist/cli/auth/browser.d.ts +2 -0
  5. package/dist/cli/auth/browser.js +33 -0
  6. package/dist/cli/auth/device.d.ts +11 -0
  7. package/dist/cli/auth/device.js +155 -0
  8. package/dist/cli/auth/store.d.ts +29 -0
  9. package/dist/cli/auth/store.js +146 -0
  10. package/dist/cli/scaffold.js +20 -154
  11. package/dist/cli.js +1 -1
  12. package/dist/commands/index.js +4 -0
  13. package/dist/commands/login/index.d.ts +8 -0
  14. package/dist/commands/login/index.js +53 -0
  15. package/dist/commands/logout/index.d.ts +7 -0
  16. package/dist/commands/logout/index.js +54 -0
  17. package/dist/config/entries.d.ts +47 -0
  18. package/dist/config/entries.js +162 -0
  19. package/dist/config/index.d.ts +2 -0
  20. package/dist/config/index.js +5 -1
  21. package/dist/config/runtime.d.ts +9 -9
  22. package/dist/config/runtime.js +15 -15
  23. package/dist/registry/auth/access-tokens.d.ts +46 -0
  24. package/dist/registry/auth/access-tokens.js +165 -0
  25. package/dist/registry/auth/action-tokens.d.ts +66 -0
  26. package/dist/registry/auth/action-tokens.js +178 -0
  27. package/dist/registry/auth/crypto.d.ts +4 -0
  28. package/dist/registry/auth/crypto.js +16 -0
  29. package/dist/registry/auth/database.d.ts +7 -0
  30. package/dist/registry/auth/database.js +22 -0
  31. package/dist/registry/auth/device-authorizations.d.ts +51 -0
  32. package/dist/registry/auth/device-authorizations.js +170 -0
  33. package/dist/registry/auth/gravatar.d.ts +1 -0
  34. package/dist/registry/auth/gravatar.js +8 -0
  35. package/dist/registry/auth/index.d.ts +11 -0
  36. package/dist/registry/auth/index.js +72 -0
  37. package/dist/registry/auth/installation.d.ts +42 -0
  38. package/dist/registry/auth/installation.js +105 -0
  39. package/dist/registry/auth/password.d.ts +12 -0
  40. package/dist/registry/auth/password.js +58 -0
  41. package/dist/registry/auth/rate-limits.d.ts +22 -0
  42. package/dist/registry/auth/rate-limits.js +75 -0
  43. package/dist/registry/auth/types.d.ts +26 -0
  44. package/dist/registry/auth/types.js +9 -0
  45. package/dist/registry/auth/user-record.d.ts +13 -0
  46. package/dist/registry/auth/user-record.js +16 -0
  47. package/dist/registry/auth/users.d.ts +31 -0
  48. package/dist/registry/auth/users.js +135 -0
  49. package/dist/registry/auth/validation.d.ts +3 -0
  50. package/dist/registry/auth/validation.js +28 -0
  51. package/dist/registry/checkout/assets.js +4 -2
  52. package/dist/registry/checkout/index.d.ts +1 -1
  53. package/dist/registry/checkout/index.js +101 -23
  54. package/dist/registry/checkout/tokens.js +4 -2
  55. package/dist/registry/connection.d.ts +9 -0
  56. package/dist/registry/connection.js +29 -1
  57. package/dist/registry/db/driver.d.ts +5 -4
  58. package/dist/registry/db/driver.js +5 -4
  59. package/dist/registry/db/index.d.ts +1 -1
  60. package/dist/registry/db/index.js +7 -1
  61. package/dist/registry/db/schema.d.ts +2521 -750
  62. package/dist/registry/db/schema.js +126 -1
  63. package/dist/registry/publish/assets.js +1 -1
  64. package/dist/registry/publish/errors.js +3 -3
  65. package/dist/registry/publish/index.d.ts +1 -1
  66. package/dist/registry/publish/index.js +9 -9
  67. package/dist/registry/publish/tokens.js +1 -1
  68. package/dist/types/config.d.ts +4 -1
  69. package/docs/cli.md +4 -0
  70. package/drizzle/0005_add_registry_auth.sql +95 -0
  71. package/drizzle/meta/0005_snapshot.json +1926 -0
  72. package/drizzle/meta/_journal.json +8 -1
  73. package/package.json +5 -4
  74. package/src/app/components/Auth/AccountLayout.tsx +87 -0
  75. package/src/app/components/Auth/AuthControls.tsx +99 -0
  76. package/src/app/components/Auth/AuthShell.tsx +73 -0
  77. package/src/app/components/Auth/RegistryInstallGate.tsx +23 -0
  78. package/src/app/components/Auth/RegistrySessionProvider.tsx +15 -0
  79. package/src/app/components/Auth/api.ts +20 -0
  80. package/src/app/components/Layout/Header.tsx +2 -0
  81. package/src/app/components/Navigation/MobileNav.tsx +2 -0
  82. package/src/app/lib/auth/api.ts +113 -0
  83. package/src/app/lib/auth/config.ts +175 -0
  84. package/src/app/lib/auth/email.ts +34 -0
  85. package/src/app/lib/docs-api/runtime-config.ts +0 -14
  86. package/src/app/lib/registry-api/auth.ts +29 -46
  87. package/src/app/lib/registry-api/errors.ts +2 -2
  88. package/src/app/lib/registry-api/handler.ts +19 -14
  89. package/src/app/lib/registry-api/token-transfer.ts +1 -1
  90. package/src/app/lib/registry-api/transfer.ts +4 -4
  91. package/src/app/next.config.mjs +12 -3
  92. package/src/app/pages/_app.tsx +10 -4
  93. package/src/app/pages/account/index.tsx +153 -0
  94. package/src/app/pages/account/tokens.tsx +230 -0
  95. package/src/app/pages/account/users.tsx +310 -0
  96. package/src/app/pages/api/account/index.ts +26 -0
  97. package/src/app/pages/api/account/tokens/[id].ts +17 -0
  98. package/src/app/pages/api/account/tokens/index.ts +44 -0
  99. package/src/app/pages/api/admin/users/[id]/[action].ts +89 -0
  100. package/src/app/pages/api/admin/users/index.ts +53 -0
  101. package/src/app/pages/api/auth/[...nextauth].ts +4 -0
  102. package/src/app/pages/api/auth/request-reset.ts +53 -0
  103. package/src/app/pages/api/auth/reset-password.ts +29 -0
  104. package/src/app/pages/api/install.ts +102 -0
  105. package/src/app/pages/api/oauth/device/approve.ts +23 -0
  106. package/src/app/pages/api/oauth/device/index.ts +41 -0
  107. package/src/app/pages/api/oauth/revoke.ts +24 -0
  108. package/src/app/pages/api/oauth/token.ts +31 -0
  109. package/src/app/pages/api/registry/health.ts +4 -3
  110. package/src/app/pages/api/registry/transfer/[kind]/[id].ts +3 -3
  111. package/src/app/pages/api/registry/transfer/assets/[collection].ts +2 -1
  112. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +2 -1
  113. package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +2 -2
  114. package/src/app/pages/cli/device.tsx +143 -0
  115. package/src/app/pages/install.tsx +336 -0
  116. package/src/app/pages/login.tsx +117 -0
  117. package/src/app/pages/reset-password.tsx +159 -0
  118. package/src/app/types/next-auth.d.ts +27 -0
  119. package/src/app-builder/build.ts +22 -5
  120. package/src/app-builder/client-config.ts +10 -9
  121. package/src/cli/auth/browser.ts +29 -0
  122. package/src/cli/auth/device.ts +181 -0
  123. package/src/cli/auth/store.ts +137 -0
  124. package/src/cli/scaffold.ts +20 -197
  125. package/src/cli.ts +1 -1
  126. package/src/commands/index.ts +4 -0
  127. package/src/commands/login/index.ts +48 -0
  128. package/src/commands/logout/index.ts +51 -0
  129. package/src/config/entries.ts +175 -0
  130. package/src/config/index.ts +4 -0
  131. package/src/config/runtime.ts +15 -14
  132. package/src/registry/auth/access-tokens.ts +198 -0
  133. package/src/registry/auth/action-tokens.ts +225 -0
  134. package/src/registry/auth/crypto.ts +13 -0
  135. package/src/registry/auth/database.ts +15 -0
  136. package/src/registry/auth/device-authorizations.ts +223 -0
  137. package/src/registry/auth/gravatar.ts +6 -0
  138. package/src/registry/auth/index.ts +89 -0
  139. package/src/registry/auth/installation.ts +117 -0
  140. package/src/registry/auth/password.ts +46 -0
  141. package/src/registry/auth/rate-limits.ts +84 -0
  142. package/src/registry/auth/types.ts +34 -0
  143. package/src/registry/auth/user-record.ts +25 -0
  144. package/src/registry/auth/users.ts +145 -0
  145. package/src/registry/auth/validation.ts +24 -0
  146. package/src/registry/checkout/assets.ts +4 -2
  147. package/src/registry/checkout/index.ts +114 -24
  148. package/src/registry/checkout/tokens.ts +4 -2
  149. package/src/registry/connection.ts +23 -0
  150. package/src/registry/db/driver.ts +5 -4
  151. package/src/registry/db/index.ts +12 -0
  152. package/src/registry/db/schema.ts +174 -1
  153. package/src/registry/publish/assets.ts +1 -1
  154. package/src/registry/publish/errors.ts +3 -3
  155. package/src/registry/publish/index.ts +9 -9
  156. package/src/registry/publish/tokens.ts +1 -1
  157. package/src/types/config.ts +4 -1
  158. package/templates/default/README.md.tpl +18 -0
  159. package/templates/default/gitignore.tpl +1 -0
  160. package/templates/default/handoff.config.js.tpl +0 -1
  161. package/templates/default/handoff.config.ts.tpl +0 -1
  162. package/templates/default/handoff.config.with-examples.js.tpl +0 -1
  163. package/templates/default/handoff.config.with-examples.ts.tpl +0 -1
@@ -0,0 +1,54 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const __1 = __importDefault(require("../../"));
16
+ const device_1 = require("../../cli/auth/device");
17
+ const store_1 = require("../../cli/auth/store");
18
+ const logger_1 = require("../../utils/logger");
19
+ const utils_1 = require("../utils");
20
+ const command = {
21
+ command: 'logout',
22
+ describe: 'Revoke and remove saved registry credentials',
23
+ builder: (yargs) => (0, utils_1.getSharedOptions)(yargs).option('url', {
24
+ type: 'string',
25
+ describe: 'Revoke credentials only when they belong to this exact registry URL',
26
+ }),
27
+ handler: (args) => __awaiter(void 0, void 0, void 0, function* () {
28
+ const handoff = new __1.default(args.debug, args.force);
29
+ const auth = yield (0, store_1.readCliAuth)(handoff.workingPath);
30
+ if (args.url && auth && !(0, store_1.cliAuthMatchesRegistry)(auth, args.url)) {
31
+ logger_1.Logger.error(`The saved login belongs to ${auth.remoteUrl}, not ${(0, store_1.normalizeRegistryUrl)(args.url)}. No credentials were changed.`);
32
+ process.exitCode = 1;
33
+ return;
34
+ }
35
+ if (auth) {
36
+ try {
37
+ yield (0, device_1.revokeAccessToken)(auth);
38
+ }
39
+ catch (error) {
40
+ logger_1.Logger.warn(`Could not revoke the registry token remotely (${error instanceof Error ? error.message : String(error)}). ` +
41
+ 'It will still be removed from this workspace.');
42
+ }
43
+ }
44
+ try {
45
+ yield (0, store_1.clearCliAuth)(handoff.workingPath);
46
+ logger_1.Logger.success(`Logged out. Removed ${(0, store_1.cliAuthFilePath)(handoff.workingPath)}.`);
47
+ }
48
+ catch (error) {
49
+ logger_1.Logger.error(`Could not remove saved credentials: ${error instanceof Error ? error.message : String(error)}`);
50
+ process.exitCode = 1;
51
+ }
52
+ }),
53
+ };
54
+ exports.default = command;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Registering directory-list entries (`entries.components` / `entries.patterns`) in the handoff
3
+ * config. Both scaffolding new component stubs and checking entities out of a registry need a
4
+ * freshly-written entity to be discoverable by the workspace build, so both have to handle every
5
+ * way a path can be declared:
6
+ *
7
+ * - a collection directory (e.g. `"components"`) whose subdirectories are entities: a new sibling
8
+ * is auto-discovered at runtime, so nothing needs writing to the config;
9
+ * - an individual entity directory (e.g. `"components/button"`): the entity stays invisible until
10
+ * its own path is added to the list.
11
+ *
12
+ * {@link isEntryCovered} answers "does this already load?" by reusing the runtime expansion
13
+ * ({@link getComponentsForPath}); {@link writeEntries} appends the paths that don't, across the
14
+ * `.json`, `.ts`, `.js`, and `.cjs` config formats.
15
+ */
16
+ import { Config } from '../types/config';
17
+ /** The `entries` keys whose values are declared as a list of directory paths. */
18
+ export type EntryKind = 'components' | 'patterns';
19
+ /** Minimal handoff shape needed here; avoids importing the full Handoff class (circular dep). */
20
+ interface ConfigContext {
21
+ config?: Config;
22
+ workingPath: string;
23
+ }
24
+ /** Outcome of {@link writeEntries}: `added` on success, `unsupported` when the config couldn't be edited. */
25
+ export interface WriteEntriesResult {
26
+ status: 'added' | 'unsupported';
27
+ /** The config file written (or that would need editing); null only when none exists and creation failed. */
28
+ configPath: string | null;
29
+ /** Workspace-relative POSIX paths written into the config. */
30
+ added: string[];
31
+ /** Workspace-relative POSIX paths the caller must add manually (`status: 'unsupported'`). */
32
+ pending: string[];
33
+ }
34
+ /**
35
+ * True when `targetDir` already loads through an existing `entries[kind]` declaration: listed
36
+ * directly, or sitting under a declared collection directory that runtime discovery expands.
37
+ * Reuses the same expansion the runtime uses, so every declaration style is covered.
38
+ */
39
+ export declare const isEntryCovered: (handoff: ConfigContext, kind: EntryKind, targetDir: string) => boolean;
40
+ /**
41
+ * Add `targetDirs` to `entries[kind]` in the workspace config so the build discovers them. Callers
42
+ * should first drop already-loading dirs via {@link isEntryCovered}. Paths are stored workspace-
43
+ * relative; a `.json` config is edited losslessly, a code config best-effort. When no config file
44
+ * exists a minimal `handoff.config.json` is created.
45
+ */
46
+ export declare const writeEntries: (handoff: ConfigContext, kind: EntryKind, targetDirs: string[]) => Promise<WriteEntriesResult>;
47
+ export {};
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ /**
3
+ * Registering directory-list entries (`entries.components` / `entries.patterns`) in the handoff
4
+ * config. Both scaffolding new component stubs and checking entities out of a registry need a
5
+ * freshly-written entity to be discoverable by the workspace build, so both have to handle every
6
+ * way a path can be declared:
7
+ *
8
+ * - a collection directory (e.g. `"components"`) whose subdirectories are entities: a new sibling
9
+ * is auto-discovered at runtime, so nothing needs writing to the config;
10
+ * - an individual entity directory (e.g. `"components/button"`): the entity stays invisible until
11
+ * its own path is added to the list.
12
+ *
13
+ * {@link isEntryCovered} answers "does this already load?" by reusing the runtime expansion
14
+ * ({@link getComponentsForPath}); {@link writeEntries} appends the paths that don't, across the
15
+ * `.json`, `.ts`, `.js`, and `.cjs` config formats.
16
+ */
17
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
18
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
19
+ return new (P || (P = Promise))(function (resolve, reject) {
20
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
21
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
22
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
23
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
24
+ });
25
+ };
26
+ var __importDefault = (this && this.__importDefault) || function (mod) {
27
+ return (mod && mod.__esModule) ? mod : { "default": mod };
28
+ };
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.writeEntries = exports.isEntryCovered = void 0;
31
+ const fs_extra_1 = __importDefault(require("fs-extra"));
32
+ const path_1 = __importDefault(require("path"));
33
+ const path_2 = require("../utils/path");
34
+ const runtime_1 = require("./runtime");
35
+ /** Config files in precedence order; the first that exists is the one we mutate. */
36
+ const CONFIG_FILES = ['handoff.config.ts', 'handoff.config.js', 'handoff.config.cjs', 'handoff.config.json'];
37
+ /** Workspace-relative, POSIX-separated path used as a config entry value. */
38
+ const toEntryPath = (handoff, targetDir) => path_1.default.relative(handoff.workingPath, targetDir).split(path_1.default.sep).join('/');
39
+ /**
40
+ * True when `targetDir` already loads through an existing `entries[kind]` declaration: listed
41
+ * directly, or sitting under a declared collection directory that runtime discovery expands.
42
+ * Reuses the same expansion the runtime uses, so every declaration style is covered.
43
+ */
44
+ const isEntryCovered = (handoff, kind, targetDir) => {
45
+ var _a, _b;
46
+ const configured = (_b = (_a = handoff.config) === null || _a === void 0 ? void 0 : _a.entries) === null || _b === void 0 ? void 0 : _b[kind];
47
+ if (!(configured === null || configured === void 0 ? void 0 : configured.length)) {
48
+ return false;
49
+ }
50
+ return configured
51
+ .flatMap((entry) => (0, runtime_1.getComponentsForPath)(path_1.default.resolve(handoff.workingPath, entry)))
52
+ .some((dir) => (0, path_2.arePathsEqual)(dir, targetDir));
53
+ };
54
+ exports.isEntryCovered = isEntryCovered;
55
+ /** Format an entry array as source, matching the surrounding indentation of a code config. */
56
+ const formatEntryArray = (paths, indent) => {
57
+ if (paths.length === 0)
58
+ return '[]';
59
+ if (paths.length === 1)
60
+ return `['${paths[0]}']`;
61
+ return `[\n${paths.map((entry) => `${indent}'${entry}',`).join('\n')}\n${indent.slice(2)}]`;
62
+ };
63
+ /** Append entry paths to a structured `.json` config (lossless read/modify/write). */
64
+ const addToJsonConfig = (configPath, kind, relPaths) => __awaiter(void 0, void 0, void 0, function* () {
65
+ var _a;
66
+ try {
67
+ const config = yield fs_extra_1.default.readJSON(configPath);
68
+ config.entries = (_a = config.entries) !== null && _a !== void 0 ? _a : {};
69
+ const existing = Array.isArray(config.entries[kind]) ? config.entries[kind] : [];
70
+ const existingSet = new Set(existing);
71
+ for (const relPath of relPaths) {
72
+ if (!existingSet.has(relPath))
73
+ existing.push(relPath);
74
+ }
75
+ config.entries[kind] = existing;
76
+ yield fs_extra_1.default.writeJSON(configPath, config, { spaces: 2 });
77
+ return true;
78
+ }
79
+ catch (_b) {
80
+ return false;
81
+ }
82
+ });
83
+ /**
84
+ * Best-effort splice of entry paths into an executable `.ts` / `.js` / `.cjs` config. These are
85
+ * modules, not data, so there's no lossless structured write; if a computed or spread `entries`
86
+ * array can't be edited textually, the caller falls back to printing the paths for the user to add.
87
+ */
88
+ const addToCodeConfig = (configPath, kind, relPaths) => __awaiter(void 0, void 0, void 0, function* () {
89
+ try {
90
+ let content = yield fs_extra_1.default.readFile(configPath, 'utf8');
91
+ const arrayBlock = formatEntryArray(relPaths, ' ');
92
+ // 1) An `entries: { ... <kind>: [ ... ] }` array already exists, so merge into it.
93
+ const hasKeyArray = new RegExp(`entries\\s*:\\s*\\{[\\s\\S]*?${kind}\\s*:`).test(content);
94
+ const arrayRegex = new RegExp(`(${kind}\\s*:\\s*\\[)([^\\]]*)(\\])`);
95
+ const match = hasKeyArray ? content.match(arrayRegex) : null;
96
+ if (match) {
97
+ const existing = match[2]
98
+ .split(',')
99
+ .map((s) => s.trim().replace(/['"]/g, ''))
100
+ .filter(Boolean);
101
+ const existingSet = new Set(existing);
102
+ const toAdd = relPaths.filter((relPath) => !existingSet.has(relPath));
103
+ if (toAdd.length > 0) {
104
+ const indentMatch = content.match(new RegExp(`${kind}\\s*:\\s*\\[\\s*\\n(\\s*)`));
105
+ const indent = indentMatch ? indentMatch[1] : ' ';
106
+ content = content.replace(arrayRegex, `${kind}: ${formatEntryArray([...existing, ...toAdd], indent)}`);
107
+ yield fs_extra_1.default.writeFile(configPath, content, 'utf8');
108
+ }
109
+ return true;
110
+ }
111
+ // 2) An `entries` object exists but not this key, so add the key.
112
+ if (/entries\s*:\s*\{/.test(content)) {
113
+ content = content.replace(/(entries\s*:\s*\{)/, `$1\n ${kind}: ${arrayBlock},`);
114
+ yield fs_extra_1.default.writeFile(configPath, content, 'utf8');
115
+ return true;
116
+ }
117
+ // 3) No `entries` object, so insert one into the exported config object.
118
+ const entriesBlock = ` entries: {\n ${kind}: ${arrayBlock},\n },\n`;
119
+ for (const anchor of [/module\.exports\s*=\s*\{/, /export\s+default\s+\{/, /defineConfig\s*\(\s*\{/]) {
120
+ if (anchor.test(content)) {
121
+ content = content.replace(anchor, (matched) => `${matched}\n${entriesBlock}`);
122
+ yield fs_extra_1.default.writeFile(configPath, content, 'utf8');
123
+ return true;
124
+ }
125
+ }
126
+ return false;
127
+ }
128
+ catch (_a) {
129
+ return false;
130
+ }
131
+ });
132
+ /**
133
+ * Add `targetDirs` to `entries[kind]` in the workspace config so the build discovers them. Callers
134
+ * should first drop already-loading dirs via {@link isEntryCovered}. Paths are stored workspace-
135
+ * relative; a `.json` config is edited losslessly, a code config best-effort. When no config file
136
+ * exists a minimal `handoff.config.json` is created.
137
+ */
138
+ const writeEntries = (handoff, kind, targetDirs) => __awaiter(void 0, void 0, void 0, function* () {
139
+ const relPaths = [...new Set(targetDirs.map((dir) => toEntryPath(handoff, dir)))];
140
+ const configFile = CONFIG_FILES.find((file) => fs_extra_1.default.existsSync(path_1.default.resolve(handoff.workingPath, file)));
141
+ const configPath = configFile ? path_1.default.resolve(handoff.workingPath, configFile) : null;
142
+ if (relPaths.length === 0) {
143
+ return { status: 'added', configPath, added: [], pending: [] };
144
+ }
145
+ if (!configPath) {
146
+ const newConfigPath = path_1.default.resolve(handoff.workingPath, 'handoff.config.json');
147
+ try {
148
+ yield fs_extra_1.default.writeJSON(newConfigPath, { entries: { [kind]: relPaths } }, { spaces: 2 });
149
+ return { status: 'added', configPath: newConfigPath, added: relPaths, pending: [] };
150
+ }
151
+ catch (_a) {
152
+ return { status: 'unsupported', configPath: null, added: [], pending: relPaths };
153
+ }
154
+ }
155
+ const ok = configPath.endsWith('.json')
156
+ ? yield addToJsonConfig(configPath, kind, relPaths)
157
+ : yield addToCodeConfig(configPath, kind, relPaths);
158
+ return ok
159
+ ? { status: 'added', configPath, added: relPaths, pending: [] }
160
+ : { status: 'unsupported', configPath, added: [], pending: relPaths };
161
+ });
162
+ exports.writeEntries = writeEntries;
@@ -4,3 +4,5 @@ export { initConfig, initConfigWithMetadata } from './loader';
4
4
  export { defineConfig } from './helpers';
5
5
  export { initRuntimeConfig } from './runtime';
6
6
  export { validateConfig } from './validator';
7
+ export { isEntryCovered, writeEntries } from './entries';
8
+ export type { EntryKind, WriteEntriesResult } from './entries';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.validateConfig = exports.initRuntimeConfig = exports.defineConfig = exports.initConfigWithMetadata = exports.initConfig = exports.getClientConfig = exports.defaultConfig = void 0;
3
+ exports.writeEntries = exports.isEntryCovered = exports.validateConfig = exports.initRuntimeConfig = exports.defineConfig = exports.initConfigWithMetadata = exports.initConfig = exports.getClientConfig = exports.defaultConfig = void 0;
4
4
  // Config defaults and client config
5
5
  var defaults_1 = require("./defaults");
6
6
  Object.defineProperty(exports, "defaultConfig", { enumerable: true, get: function () { return defaults_1.defaultConfig; } });
@@ -18,3 +18,7 @@ Object.defineProperty(exports, "initRuntimeConfig", { enumerable: true, get: fun
18
18
  // Config validation
19
19
  var validator_1 = require("./validator");
20
20
  Object.defineProperty(exports, "validateConfig", { enumerable: true, get: function () { return validator_1.validateConfig; } });
21
+ // Registering directory-list entries (components/patterns)
22
+ var entries_1 = require("./entries");
23
+ Object.defineProperty(exports, "isEntryCovered", { enumerable: true, get: function () { return entries_1.isEntryCovered; } });
24
+ Object.defineProperty(exports, "writeEntries", { enumerable: true, get: function () { return entries_1.writeEntries; } });
@@ -21,17 +21,17 @@ export declare const resolveComponentDeclaration: (componentDir: string, compone
21
21
  */
22
22
  export declare const initRuntimeConfig: (handoff: HandoffContext) => [runtimeConfig: RuntimeConfig, configs: string[], configFileIndex: Map<string, ConfigFileEntry>];
23
23
  /**
24
- * Returns a list of component directories for a given path.
25
- *
26
- * This function determines whether the provided `searchPath` is:
27
- * 1. A single component directory (contains a config file named after the directory)
28
- * 2. A collection of component directories (subdirectories are components)
29
- *
30
- * A directory is considered a component if it contains a config file matching
31
- * `{dirname}.json`, `{dirname}.js`, or `{dirname}.cjs`.
24
+ * True when `searchPath` is itself a single component directory: it holds a declaration file named
25
+ * after the directory (`{dirname}.handoff.*`, or a legacy `{dirname}.{json,js,cjs}`). This is what
26
+ * separates a declared component from a collection directory.
27
+ */
28
+ export declare const isComponentDirectory: (searchPath: string) => boolean;
29
+ /**
30
+ * Returns the component directories for a given path. If `searchPath` is itself a component
31
+ * directory it's returned as-is; otherwise each subdirectory is treated as a potential component.
32
32
  *
33
33
  * @param searchPath - The absolute path to check for components.
34
- * @returns An array of string paths to component directories.
34
+ * @returns Paths to the component directories.
35
35
  */
36
36
  export declare const getComponentsForPath: (searchPath: string) => string[];
37
37
  /**
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.toLowerCaseKeysAndValues = exports.getItemsForPath = exports.getComponentsForPath = exports.initRuntimeConfig = exports.resolveComponentDeclaration = void 0;
6
+ exports.toLowerCaseKeysAndValues = exports.getItemsForPath = exports.getComponentsForPath = exports.isComponentDirectory = exports.initRuntimeConfig = exports.resolveComponentDeclaration = void 0;
7
7
  const esbuild_1 = __importDefault(require("esbuild"));
8
8
  const fs_extra_1 = __importDefault(require("fs-extra"));
9
9
  const gray_matter_1 = __importDefault(require("gray-matter"));
@@ -283,24 +283,24 @@ const collectPageMarkdownPaths = (rootDir, relativeParts = []) => {
283
283
  return results;
284
284
  };
285
285
  /**
286
- * Returns a list of component directories for a given path.
287
- *
288
- * This function determines whether the provided `searchPath` is:
289
- * 1. A single component directory (contains a config file named after the directory)
290
- * 2. A collection of component directories (subdirectories are components)
291
- *
292
- * A directory is considered a component if it contains a config file matching
293
- * `{dirname}.json`, `{dirname}.js`, or `{dirname}.cjs`.
286
+ * True when `searchPath` is itself a single component directory: it holds a declaration file named
287
+ * after the directory (`{dirname}.handoff.*`, or a legacy `{dirname}.{json,js,cjs}`). This is what
288
+ * separates a declared component from a collection directory.
289
+ */
290
+ const isComponentDirectory = (searchPath) => !!(0, exports.resolveComponentDeclaration)(searchPath, path_1.default.basename(searchPath));
291
+ exports.isComponentDirectory = isComponentDirectory;
292
+ /**
293
+ * Returns the component directories for a given path. If `searchPath` is itself a component
294
+ * directory it's returned as-is; otherwise each subdirectory is treated as a potential component.
294
295
  *
295
296
  * @param searchPath - The absolute path to check for components.
296
- * @returns An array of string paths to component directories.
297
+ * @returns Paths to the component directories.
297
298
  */
298
299
  const getComponentsForPath = (searchPath) => {
299
- const dirName = path_1.default.basename(searchPath);
300
- const hasOwnConfig = !!(0, exports.resolveComponentDeclaration)(searchPath, dirName);
301
- // Check if searchPath itself is a component directory (has a config file named after the directory)
302
- if (hasOwnConfig) {
303
- // This directory is a single component
300
+ if ((0, exports.isComponentDirectory)(searchPath)) {
301
+ return [searchPath];
302
+ }
303
+ if (!fs_extra_1.default.existsSync(searchPath)) {
304
304
  return [searchPath];
305
305
  }
306
306
  // Otherwise, treat each subdirectory as a potential component
@@ -0,0 +1,46 @@
1
+ import type { RegistryDatabase } from '../db/client';
2
+ import { type RegistryAccessScope } from '../db/schema';
3
+ import { type RegistryPrincipal, type RegistryUserRole } from './types';
4
+ export interface RegistryAccessTokenSummary {
5
+ id: string;
6
+ name: string;
7
+ scopes: RegistryAccessScope[];
8
+ expiresAt: Date;
9
+ lastUsedAt: Date | null;
10
+ revokedAt: Date | null;
11
+ createdAt: Date;
12
+ }
13
+ export type CreateRegistryAccessTokenResult = {
14
+ ok: true;
15
+ token: string;
16
+ record: RegistryAccessTokenSummary;
17
+ } | {
18
+ ok: false;
19
+ reason: 'user_not_found' | 'user_inactive' | 'scope_forbidden' | 'invalid_name' | 'invalid_expiry';
20
+ };
21
+ export declare const normalizeRegistryScopes: (scopes: readonly RegistryAccessScope[]) => RegistryAccessScope[];
22
+ export declare const registryRoleAllowsScopes: (role: RegistryUserRole, scopes: readonly RegistryAccessScope[]) => boolean;
23
+ export declare const formatRegistryAccessToken: (id: string, secret: string) => string;
24
+ export declare const parseRegistryAccessToken: (token: string) => {
25
+ id: string;
26
+ secret: string;
27
+ } | null;
28
+ export declare const createRegistryAccessToken: (db: RegistryDatabase, input: {
29
+ userId: string;
30
+ name: string;
31
+ scopes: RegistryAccessScope[];
32
+ expiresAt?: Date;
33
+ now?: Date;
34
+ }) => Promise<CreateRegistryAccessTokenResult>;
35
+ export declare const listRegistryAccessTokens: (db: RegistryDatabase, userId: string) => Promise<RegistryAccessTokenSummary[]>;
36
+ export declare const revokeRegistryAccessToken: (db: RegistryDatabase, input: {
37
+ tokenId: string;
38
+ userId: string;
39
+ now?: Date;
40
+ }) => Promise<boolean>;
41
+ export declare const revokeAllRegistryAccessTokens: (db: RegistryDatabase, userId: string, now?: Date) => Promise<number>;
42
+ /**
43
+ * Resolve an opaque bearer credential into a live principal. User state is joined on every call,
44
+ * making deactivation effective even if a defensive token-revocation write was interrupted.
45
+ */
46
+ export declare const authenticateRegistryAccessToken: (db: RegistryDatabase, token: string, now?: Date) => Promise<RegistryPrincipal | null>;
@@ -0,0 +1,165 @@
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.authenticateRegistryAccessToken = exports.revokeAllRegistryAccessTokens = exports.revokeRegistryAccessToken = exports.listRegistryAccessTokens = exports.createRegistryAccessToken = exports.parseRegistryAccessToken = exports.formatRegistryAccessToken = exports.registryRoleAllowsScopes = exports.normalizeRegistryScopes = void 0;
13
+ const drizzle_orm_1 = require("drizzle-orm");
14
+ const schema_1 = require("../db/schema");
15
+ const crypto_1 = require("./crypto");
16
+ const database_1 = require("./database");
17
+ const types_1 = require("./types");
18
+ const ACCESS_TOKEN_PREFIX = 'hnd';
19
+ const DEFAULT_ACCESS_TOKEN_LIFETIME_MS = 365 * 24 * 60 * 60 * 1000;
20
+ const isRegistryScope = (value) => value === types_1.REGISTRY_READ_SCOPE || value === types_1.REGISTRY_WRITE_SCOPE;
21
+ const normalizeRegistryScopes = (scopes) => {
22
+ const unique = new Set(scopes.filter(isRegistryScope));
23
+ return [types_1.REGISTRY_READ_SCOPE, types_1.REGISTRY_WRITE_SCOPE].filter((scope) => unique.has(scope));
24
+ };
25
+ exports.normalizeRegistryScopes = normalizeRegistryScopes;
26
+ const registryRoleAllowsScopes = (role, scopes) => role === 'admin' || !scopes.includes(types_1.REGISTRY_WRITE_SCOPE);
27
+ exports.registryRoleAllowsScopes = registryRoleAllowsScopes;
28
+ const formatRegistryAccessToken = (id, secret) => `${ACCESS_TOKEN_PREFIX}_${id}_${secret}`;
29
+ exports.formatRegistryAccessToken = formatRegistryAccessToken;
30
+ const parseRegistryAccessToken = (token) => {
31
+ const match = /^hnd_([A-Za-z0-9_-]{16})_([A-Za-z0-9_-]{43})$/.exec(token.trim());
32
+ return match ? { id: match[1], secret: match[2] } : null;
33
+ };
34
+ exports.parseRegistryAccessToken = parseRegistryAccessToken;
35
+ const createRegistryAccessTokenInTransaction = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
36
+ var _a, _b;
37
+ const name = input.name.trim();
38
+ if (!name || name.length > 120)
39
+ return { ok: false, reason: 'invalid_name' };
40
+ const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
41
+ const expiresAt = (_b = input.expiresAt) !== null && _b !== void 0 ? _b : new Date(now.getTime() + DEFAULT_ACCESS_TOKEN_LIFETIME_MS);
42
+ if (!(expiresAt instanceof Date) || !Number.isFinite(expiresAt.getTime()) || expiresAt <= now) {
43
+ return { ok: false, reason: 'invalid_expiry' };
44
+ }
45
+ const [user] = yield db
46
+ .select({ id: schema_1.registryUsers.id, role: schema_1.registryUsers.role, status: schema_1.registryUsers.status })
47
+ .from(schema_1.registryUsers)
48
+ .where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, input.userId))
49
+ .limit(1)
50
+ .for('update');
51
+ if (!user)
52
+ return { ok: false, reason: 'user_not_found' };
53
+ if (user.status !== 'active')
54
+ return { ok: false, reason: 'user_inactive' };
55
+ const scopes = (0, exports.normalizeRegistryScopes)(input.scopes);
56
+ if (!scopes.length)
57
+ scopes.push(types_1.REGISTRY_READ_SCOPE);
58
+ if (!(0, exports.registryRoleAllowsScopes)(user.role, scopes))
59
+ return { ok: false, reason: 'scope_forbidden' };
60
+ const id = (0, crypto_1.createOpaqueSecret)(12);
61
+ const secret = (0, crypto_1.createOpaqueSecret)(32);
62
+ const [record] = yield db
63
+ .insert(schema_1.registryAccessTokens)
64
+ .values({
65
+ id,
66
+ userId: user.id,
67
+ name,
68
+ secretHash: (0, crypto_1.hashSecret)(secret),
69
+ scopes,
70
+ expiresAt,
71
+ })
72
+ .returning({
73
+ id: schema_1.registryAccessTokens.id,
74
+ name: schema_1.registryAccessTokens.name,
75
+ scopes: schema_1.registryAccessTokens.scopes,
76
+ expiresAt: schema_1.registryAccessTokens.expiresAt,
77
+ lastUsedAt: schema_1.registryAccessTokens.lastUsedAt,
78
+ revokedAt: schema_1.registryAccessTokens.revokedAt,
79
+ createdAt: schema_1.registryAccessTokens.createdAt,
80
+ });
81
+ return { ok: true, token: (0, exports.formatRegistryAccessToken)(id, secret), record };
82
+ });
83
+ const createRegistryAccessToken = (db, input) => __awaiter(void 0, void 0, void 0, function* () { return (0, database_1.withRegistryTransaction)(db, (tx) => createRegistryAccessTokenInTransaction(tx, input)); });
84
+ exports.createRegistryAccessToken = createRegistryAccessToken;
85
+ const listRegistryAccessTokens = (db, userId) => __awaiter(void 0, void 0, void 0, function* () {
86
+ return db
87
+ .select({
88
+ id: schema_1.registryAccessTokens.id,
89
+ name: schema_1.registryAccessTokens.name,
90
+ scopes: schema_1.registryAccessTokens.scopes,
91
+ expiresAt: schema_1.registryAccessTokens.expiresAt,
92
+ lastUsedAt: schema_1.registryAccessTokens.lastUsedAt,
93
+ revokedAt: schema_1.registryAccessTokens.revokedAt,
94
+ createdAt: schema_1.registryAccessTokens.createdAt,
95
+ })
96
+ .from(schema_1.registryAccessTokens)
97
+ .where((0, drizzle_orm_1.eq)(schema_1.registryAccessTokens.userId, userId))
98
+ .orderBy(schema_1.registryAccessTokens.createdAt);
99
+ });
100
+ exports.listRegistryAccessTokens = listRegistryAccessTokens;
101
+ const revokeRegistryAccessToken = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
102
+ var _a;
103
+ const rows = yield db
104
+ .update(schema_1.registryAccessTokens)
105
+ .set({ revokedAt: (_a = input.now) !== null && _a !== void 0 ? _a : new Date() })
106
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryAccessTokens.id, input.tokenId), (0, drizzle_orm_1.eq)(schema_1.registryAccessTokens.userId, input.userId), (0, drizzle_orm_1.isNull)(schema_1.registryAccessTokens.revokedAt)))
107
+ .returning({ id: schema_1.registryAccessTokens.id });
108
+ return rows.length > 0;
109
+ });
110
+ exports.revokeRegistryAccessToken = revokeRegistryAccessToken;
111
+ const revokeAllRegistryAccessTokens = (db_1, userId_1, ...args_1) => __awaiter(void 0, [db_1, userId_1, ...args_1], void 0, function* (db, userId, now = new Date()) {
112
+ const rows = yield db
113
+ .update(schema_1.registryAccessTokens)
114
+ .set({ revokedAt: now })
115
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryAccessTokens.userId, userId), (0, drizzle_orm_1.isNull)(schema_1.registryAccessTokens.revokedAt)))
116
+ .returning({ id: schema_1.registryAccessTokens.id });
117
+ return rows.length;
118
+ });
119
+ exports.revokeAllRegistryAccessTokens = revokeAllRegistryAccessTokens;
120
+ /**
121
+ * Resolve an opaque bearer credential into a live principal. User state is joined on every call,
122
+ * making deactivation effective even if a defensive token-revocation write was interrupted.
123
+ */
124
+ const authenticateRegistryAccessToken = (db_1, token_1, ...args_1) => __awaiter(void 0, [db_1, token_1, ...args_1], void 0, function* (db, token, now = new Date()) {
125
+ const parsed = (0, exports.parseRegistryAccessToken)(token);
126
+ if (!parsed)
127
+ return null;
128
+ const [row] = yield db
129
+ .select({
130
+ tokenId: schema_1.registryAccessTokens.id,
131
+ secretHash: schema_1.registryAccessTokens.secretHash,
132
+ scopes: schema_1.registryAccessTokens.scopes,
133
+ expiresAt: schema_1.registryAccessTokens.expiresAt,
134
+ revokedAt: schema_1.registryAccessTokens.revokedAt,
135
+ userId: schema_1.registryUsers.id,
136
+ email: schema_1.registryUsers.email,
137
+ name: schema_1.registryUsers.name,
138
+ role: schema_1.registryUsers.role,
139
+ status: schema_1.registryUsers.status,
140
+ })
141
+ .from(schema_1.registryAccessTokens)
142
+ .innerJoin(schema_1.registryUsers, (0, drizzle_orm_1.eq)(schema_1.registryUsers.id, schema_1.registryAccessTokens.userId))
143
+ .where((0, drizzle_orm_1.eq)(schema_1.registryAccessTokens.id, parsed.id))
144
+ .limit(1);
145
+ if (!row || row.status !== 'active' || row.revokedAt || row.expiresAt <= now || !(0, crypto_1.secretHashMatches)(parsed.secret, row.secretHash)) {
146
+ return null;
147
+ }
148
+ const scopes = (0, exports.normalizeRegistryScopes)(Array.isArray(row.scopes) ? row.scopes : []);
149
+ const updated = yield db
150
+ .update(schema_1.registryAccessTokens)
151
+ .set({ lastUsedAt: now })
152
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryAccessTokens.id, row.tokenId), (0, drizzle_orm_1.isNull)(schema_1.registryAccessTokens.revokedAt), (0, drizzle_orm_1.gt)(schema_1.registryAccessTokens.expiresAt, now)))
153
+ .returning({ id: schema_1.registryAccessTokens.id });
154
+ if (!updated.length)
155
+ return null;
156
+ return {
157
+ tokenId: row.tokenId,
158
+ userId: row.userId,
159
+ email: row.email,
160
+ name: row.name,
161
+ role: row.role,
162
+ scopes,
163
+ };
164
+ });
165
+ exports.authenticateRegistryAccessToken = authenticateRegistryAccessToken;
@@ -0,0 +1,66 @@
1
+ import type { RegistryDatabase } from '../db/client';
2
+ import { type RegistryUserRole } from '../db/schema';
3
+ import type { RegistryUser } from './types';
4
+ export declare const INVITATION_LIFETIME_MS: number;
5
+ export declare const PASSWORD_RESET_LIFETIME_MS: number;
6
+ export type CreateUserInvitationResult = {
7
+ ok: true;
8
+ user: RegistryUser;
9
+ token: string;
10
+ expiresAt: Date;
11
+ } | {
12
+ ok: false;
13
+ reason: 'invalid_email' | 'invalid_name' | 'invalid_role' | 'email_exists';
14
+ };
15
+ export type ResendUserInvitationResult = {
16
+ ok: true;
17
+ user: RegistryUser;
18
+ token: string;
19
+ expiresAt: Date;
20
+ } | {
21
+ ok: false;
22
+ reason: 'not_found' | 'already_accepted' | 'deactivated';
23
+ };
24
+ export type ConsumeAuthActionResult = {
25
+ ok: true;
26
+ user: RegistryUser;
27
+ } | {
28
+ ok: false;
29
+ reason: 'invalid_or_expired_token' | 'invalid_password';
30
+ error?: string;
31
+ };
32
+ export declare const createUserInvitation: (db: RegistryDatabase, input: {
33
+ email: string;
34
+ name: string;
35
+ role: RegistryUserRole;
36
+ now?: Date;
37
+ expiresAt?: Date;
38
+ }) => Promise<CreateUserInvitationResult>;
39
+ export declare const resendUserInvitation: (db: RegistryDatabase, input: {
40
+ userId: string;
41
+ now?: Date;
42
+ expiresAt?: Date;
43
+ }) => Promise<ResendUserInvitationResult>;
44
+ export declare const acceptUserInvitation: (db: RegistryDatabase, input: {
45
+ token: string;
46
+ password: string;
47
+ now?: Date;
48
+ }) => Promise<ConsumeAuthActionResult>;
49
+ /**
50
+ * Enumeration-safe reset request. The token is returned only to the trusted mail-delivery caller;
51
+ * unknown/inactive emails return the same successful shape with a null token.
52
+ */
53
+ export declare const createPasswordReset: (db: RegistryDatabase, email: string, options?: {
54
+ now?: Date;
55
+ expiresAt?: Date;
56
+ }) => Promise<{
57
+ ok: true;
58
+ user: RegistryUser | null;
59
+ token: string | null;
60
+ expiresAt: Date | null;
61
+ }>;
62
+ export declare const resetPassword: (db: RegistryDatabase, input: {
63
+ token: string;
64
+ password: string;
65
+ now?: Date;
66
+ }) => Promise<ConsumeAuthActionResult>;