handoff-app 2.0.0-2 → 2.0.0-3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -83
- package/dist/app-builder/build.js +21 -4
- package/dist/app-builder/client-config.js +9 -8
- package/dist/cli/auth/browser.d.ts +2 -0
- package/dist/cli/auth/browser.js +33 -0
- package/dist/cli/auth/device.d.ts +11 -0
- package/dist/cli/auth/device.js +155 -0
- package/dist/cli/auth/store.d.ts +29 -0
- package/dist/cli/auth/store.js +146 -0
- package/dist/cli/scaffold.js +20 -154
- package/dist/commands/index.js +4 -0
- package/dist/commands/login/index.d.ts +8 -0
- package/dist/commands/login/index.js +53 -0
- package/dist/commands/logout/index.d.ts +7 -0
- package/dist/commands/logout/index.js +54 -0
- package/dist/config/entries.d.ts +47 -0
- package/dist/config/entries.js +162 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +5 -1
- package/dist/config/runtime.d.ts +9 -9
- package/dist/config/runtime.js +15 -15
- package/dist/registry/auth/access-tokens.d.ts +46 -0
- package/dist/registry/auth/access-tokens.js +165 -0
- package/dist/registry/auth/action-tokens.d.ts +66 -0
- package/dist/registry/auth/action-tokens.js +178 -0
- package/dist/registry/auth/crypto.d.ts +4 -0
- package/dist/registry/auth/crypto.js +16 -0
- package/dist/registry/auth/database.d.ts +7 -0
- package/dist/registry/auth/database.js +22 -0
- package/dist/registry/auth/device-authorizations.d.ts +51 -0
- package/dist/registry/auth/device-authorizations.js +170 -0
- package/dist/registry/auth/gravatar.d.ts +1 -0
- package/dist/registry/auth/gravatar.js +8 -0
- package/dist/registry/auth/index.d.ts +11 -0
- package/dist/registry/auth/index.js +72 -0
- package/dist/registry/auth/installation.d.ts +42 -0
- package/dist/registry/auth/installation.js +105 -0
- package/dist/registry/auth/password.d.ts +12 -0
- package/dist/registry/auth/password.js +58 -0
- package/dist/registry/auth/rate-limits.d.ts +22 -0
- package/dist/registry/auth/rate-limits.js +75 -0
- package/dist/registry/auth/types.d.ts +26 -0
- package/dist/registry/auth/types.js +9 -0
- package/dist/registry/auth/user-record.d.ts +13 -0
- package/dist/registry/auth/user-record.js +16 -0
- package/dist/registry/auth/users.d.ts +31 -0
- package/dist/registry/auth/users.js +135 -0
- package/dist/registry/auth/validation.d.ts +3 -0
- package/dist/registry/auth/validation.js +28 -0
- package/dist/registry/checkout/assets.js +4 -2
- package/dist/registry/checkout/index.d.ts +1 -1
- package/dist/registry/checkout/index.js +101 -23
- package/dist/registry/checkout/tokens.js +4 -2
- package/dist/registry/connection.d.ts +9 -0
- package/dist/registry/connection.js +29 -1
- package/dist/registry/db/driver.d.ts +5 -4
- package/dist/registry/db/driver.js +5 -4
- package/dist/registry/db/index.d.ts +1 -1
- package/dist/registry/db/index.js +7 -1
- package/dist/registry/db/schema.d.ts +2521 -750
- package/dist/registry/db/schema.js +126 -1
- package/dist/registry/publish/assets.js +1 -1
- package/dist/registry/publish/errors.js +3 -3
- package/dist/registry/publish/index.d.ts +1 -1
- package/dist/registry/publish/index.js +9 -9
- package/dist/registry/publish/tokens.js +1 -1
- package/dist/types/config.d.ts +4 -1
- package/docs/cli.md +4 -0
- package/drizzle/0005_add_registry_auth.sql +95 -0
- package/drizzle/meta/0005_snapshot.json +1926 -0
- package/drizzle/meta/_journal.json +8 -1
- package/package.json +5 -4
- package/src/app/components/Auth/AccountLayout.tsx +87 -0
- package/src/app/components/Auth/AuthControls.tsx +99 -0
- package/src/app/components/Auth/AuthShell.tsx +73 -0
- package/src/app/components/Auth/RegistryInstallGate.tsx +23 -0
- package/src/app/components/Auth/RegistrySessionProvider.tsx +15 -0
- package/src/app/components/Auth/api.ts +20 -0
- package/src/app/components/Layout/Header.tsx +2 -0
- package/src/app/components/Navigation/MobileNav.tsx +2 -0
- package/src/app/lib/auth/api.ts +113 -0
- package/src/app/lib/auth/config.ts +175 -0
- package/src/app/lib/auth/email.ts +34 -0
- package/src/app/lib/docs-api/runtime-config.ts +0 -14
- package/src/app/lib/registry-api/auth.ts +29 -46
- package/src/app/lib/registry-api/errors.ts +2 -2
- package/src/app/lib/registry-api/handler.ts +19 -14
- package/src/app/lib/registry-api/token-transfer.ts +1 -1
- package/src/app/lib/registry-api/transfer.ts +4 -4
- package/src/app/next.config.mjs +12 -3
- package/src/app/pages/_app.tsx +10 -4
- package/src/app/pages/account/index.tsx +153 -0
- package/src/app/pages/account/tokens.tsx +230 -0
- package/src/app/pages/account/users.tsx +310 -0
- package/src/app/pages/api/account/index.ts +26 -0
- package/src/app/pages/api/account/tokens/[id].ts +17 -0
- package/src/app/pages/api/account/tokens/index.ts +44 -0
- package/src/app/pages/api/admin/users/[id]/[action].ts +89 -0
- package/src/app/pages/api/admin/users/index.ts +53 -0
- package/src/app/pages/api/auth/[...nextauth].ts +4 -0
- package/src/app/pages/api/auth/request-reset.ts +53 -0
- package/src/app/pages/api/auth/reset-password.ts +29 -0
- package/src/app/pages/api/install.ts +102 -0
- package/src/app/pages/api/oauth/device/approve.ts +23 -0
- package/src/app/pages/api/oauth/device/index.ts +41 -0
- package/src/app/pages/api/oauth/revoke.ts +24 -0
- package/src/app/pages/api/oauth/token.ts +31 -0
- package/src/app/pages/api/registry/health.ts +4 -3
- package/src/app/pages/api/registry/transfer/[kind]/[id].ts +3 -3
- package/src/app/pages/api/registry/transfer/assets/[collection].ts +2 -1
- package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +2 -1
- package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +2 -2
- package/src/app/pages/cli/device.tsx +143 -0
- package/src/app/pages/install.tsx +336 -0
- package/src/app/pages/login.tsx +117 -0
- package/src/app/pages/reset-password.tsx +159 -0
- package/src/app/types/next-auth.d.ts +27 -0
- package/src/app-builder/build.ts +22 -5
- package/src/app-builder/client-config.ts +10 -9
- package/src/cli/auth/browser.ts +29 -0
- package/src/cli/auth/device.ts +181 -0
- package/src/cli/auth/store.ts +137 -0
- package/src/cli/scaffold.ts +20 -197
- package/src/commands/index.ts +4 -0
- package/src/commands/login/index.ts +48 -0
- package/src/commands/logout/index.ts +51 -0
- package/src/config/entries.ts +175 -0
- package/src/config/index.ts +4 -0
- package/src/config/runtime.ts +15 -14
- package/src/registry/auth/access-tokens.ts +198 -0
- package/src/registry/auth/action-tokens.ts +225 -0
- package/src/registry/auth/crypto.ts +13 -0
- package/src/registry/auth/database.ts +15 -0
- package/src/registry/auth/device-authorizations.ts +223 -0
- package/src/registry/auth/gravatar.ts +6 -0
- package/src/registry/auth/index.ts +89 -0
- package/src/registry/auth/installation.ts +117 -0
- package/src/registry/auth/password.ts +46 -0
- package/src/registry/auth/rate-limits.ts +84 -0
- package/src/registry/auth/types.ts +34 -0
- package/src/registry/auth/user-record.ts +25 -0
- package/src/registry/auth/users.ts +145 -0
- package/src/registry/auth/validation.ts +24 -0
- package/src/registry/checkout/assets.ts +4 -2
- package/src/registry/checkout/index.ts +114 -24
- package/src/registry/checkout/tokens.ts +4 -2
- package/src/registry/connection.ts +23 -0
- package/src/registry/db/driver.ts +5 -4
- package/src/registry/db/index.ts +12 -0
- package/src/registry/db/schema.ts +174 -1
- package/src/registry/publish/assets.ts +1 -1
- package/src/registry/publish/errors.ts +3 -3
- package/src/registry/publish/index.ts +9 -9
- package/src/registry/publish/tokens.ts +1 -1
- package/src/types/config.ts +4 -1
- package/templates/default/README.md.tpl +18 -0
- package/templates/default/gitignore.tpl +1 -0
- package/templates/default/handoff.config.js.tpl +0 -1
- package/templates/default/handoff.config.ts.tpl +0 -1
- package/templates/default/handoff.config.with-examples.js.tpl +0 -1
- package/templates/default/handoff.config.with-examples.ts.tpl +0 -1
|
@@ -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;
|
package/dist/config/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/config/index.js
CHANGED
|
@@ -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; } });
|
package/dist/config/runtime.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
|
34
|
+
* @returns Paths to the component directories.
|
|
35
35
|
*/
|
|
36
36
|
export declare const getComponentsForPath: (searchPath: string) => string[];
|
|
37
37
|
/**
|
package/dist/config/runtime.js
CHANGED
|
@@ -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
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
*
|
|
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
|
|
297
|
+
* @returns Paths to the component directories.
|
|
297
298
|
*/
|
|
298
299
|
const getComponentsForPath = (searchPath) => {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (
|
|
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>;
|