handoff-app 2.0.0-1 → 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 +170 -91
- package/config/docs/foundations/colors/guidelines.md +9 -0
- package/config/docs/foundations/effects/guidelines.md +9 -0
- package/config/docs/foundations/effects/resources.md +9 -0
- package/config/docs/foundations/grid.md +10 -0
- package/config/docs/foundations/icons/guidelines.md +9 -0
- package/config/docs/foundations/images/guidelines.md +9 -0
- package/config/docs/foundations/images/resources.md +9 -0
- package/config/docs/foundations/images.md +10 -0
- package/config/docs/foundations/logo/resources.md +9 -0
- package/config/docs/foundations/typography/guidelines.md +9 -0
- package/config/docs/foundations/typography/resources.md +9 -0
- package/config/docs/foundations.md +5 -3
- package/dist/app-builder/build.js +109 -49
- package/dist/app-builder/client-config.js +13 -12
- package/dist/app-builder/paths.js +7 -9
- package/dist/app-builder/static-export.js +6 -6
- package/dist/app-builder/vercel-output.js +2 -2
- package/dist/artifacts/url.d.ts +7 -7
- package/dist/artifacts/url.js +7 -7
- 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/create.js +48 -3
- package/dist/cli/scaffold.js +20 -154
- package/dist/commands/checkout/index.d.ts +6 -6
- package/dist/commands/checkout/index.js +8 -12
- 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/commands/publish/index.d.ts +6 -5
- package/dist/commands/publish/index.js +8 -11
- package/dist/commands/utils.d.ts +6 -0
- package/dist/commands/utils.js +9 -1
- 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/index.d.ts +12 -8
- package/dist/index.js +26 -10
- package/dist/registry/asset-storage/resolve.d.ts +1 -1
- package/dist/registry/asset-storage/resolve.js +1 -1
- 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 +11 -6
- package/dist/registry/checkout/index.js +159 -30
- package/dist/registry/checkout/tokens.js +4 -2
- package/dist/registry/client.d.ts +2 -1
- package/dist/registry/client.js +8 -0
- package/dist/registry/connection.d.ts +9 -0
- package/dist/registry/connection.js +29 -1
- package/dist/registry/db/client.d.ts +9 -9
- package/dist/registry/db/client.js +7 -7
- package/dist/registry/db/driver.d.ts +46 -0
- package/dist/registry/db/{adapter.js → driver.js} +22 -19
- package/dist/registry/db/index.d.ts +2 -2
- package/dist/registry/db/index.js +15 -9
- package/dist/registry/db/migrate.d.ts +1 -1
- package/dist/registry/db/migrate.js +5 -5
- 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 +9 -2
- package/dist/registry/publish/index.js +96 -10
- package/dist/registry/publish/tokens.js +1 -1
- package/dist/registry/transfer.d.ts +13 -0
- package/dist/types/config.d.ts +11 -4
- 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/drizzle.config.ts +1 -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/Foundations/DisplayLogo.tsx +3 -3
- 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 +10 -24
- 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 +31 -2
- package/src/app/lib/registry-connection.ts +2 -2
- package/src/app/next.config.mjs +13 -4
- 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/[kind]/index.ts +21 -0
- 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/foundations/icons/index.tsx +2 -2
- package/src/app/pages/foundations/icons/{[name] → library/[name]}/index.tsx +6 -6
- 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 +96 -59
- package/src/app-builder/client-config.ts +12 -11
- package/src/app-builder/paths.ts +7 -9
- package/src/app-builder/static-export.ts +6 -6
- package/src/app-builder/vercel-output.ts +2 -2
- package/src/artifacts/url.ts +7 -7
- 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/create.ts +58 -3
- package/src/cli/scaffold.ts +20 -197
- package/src/commands/checkout/index.ts +10 -15
- 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/commands/publish/index.ts +10 -14
- package/src/commands/utils.ts +12 -0
- package/src/config/entries.ts +175 -0
- package/src/config/index.ts +4 -0
- package/src/config/runtime.ts +15 -14
- package/src/index.ts +26 -12
- package/src/registry/asset-storage/resolve.ts +1 -1
- 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 +182 -30
- package/src/registry/checkout/tokens.ts +4 -2
- package/src/registry/client.ts +7 -1
- package/src/registry/connection.ts +23 -0
- package/src/registry/db/client.ts +12 -12
- package/src/registry/db/{adapter.ts → driver.ts} +24 -21
- package/src/registry/db/index.ts +16 -4
- package/src/registry/db/migrate.ts +5 -5
- 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 +108 -10
- package/src/registry/publish/tokens.ts +1 -1
- package/src/registry/transfer.ts +14 -0
- package/src/types/config.ts +11 -4
- package/templates/default/README.md.tpl +58 -1
- package/templates/default/gitignore.tpl +19 -3
- package/templates/default/handoff.config.js.tpl +13 -0
- package/templates/default/handoff.config.ts.tpl +13 -0
- package/templates/default/handoff.config.with-examples.js.tpl +13 -0
- package/templates/default/handoff.config.with-examples.ts.tpl +13 -0
- package/templates/default/package.json.tpl +2 -1
- package/templates/default/vercel.json.tpl +4 -0
- package/dist/registry/db/adapter.d.ts +0 -43
package/dist/index.d.ts
CHANGED
|
@@ -45,17 +45,21 @@ declare class Handoff {
|
|
|
45
45
|
*/
|
|
46
46
|
dbMigrate(): Promise<Handoff>;
|
|
47
47
|
/**
|
|
48
|
-
* Publish
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
48
|
+
* Publish components, patterns, or pages from this connected workspace to the configured remote
|
|
49
|
+
* registry. With `id` it publishes that one entity after a targeted build; without `id` it builds
|
|
50
|
+
* the kind once and publishes every declared entity, skipping ones whose content is unchanged on
|
|
51
|
+
* the registry (`--force` re-uploads all). Pages upload their record and markdown source because
|
|
52
|
+
* they render at runtime. The publish module is loaded lazily so the registry client and build code
|
|
53
|
+
* never enter the docs app bundle.
|
|
52
54
|
*/
|
|
53
|
-
publish(kind: 'component' | 'pattern' | 'page', id
|
|
55
|
+
publish(kind: 'component' | 'pattern' | 'page', id?: string): Promise<Handoff>;
|
|
54
56
|
/**
|
|
55
|
-
* Checkout
|
|
56
|
-
*
|
|
57
|
+
* Checkout components, patterns, or pages from the connected remote registry into this workspace.
|
|
58
|
+
* With `id` it checks out that one entity; without `id` it checks out every published entity of the
|
|
59
|
+
* kind. Overwriting existing local files requires `--force` or an interactive confirmation. The
|
|
60
|
+
* checkout module is loaded lazily so the registry client never enters the docs app bundle.
|
|
57
61
|
*/
|
|
58
|
-
checkout(kind: 'component' | 'pattern' | 'page', id
|
|
62
|
+
checkout(kind: 'component' | 'pattern' | 'page', id?: string): Promise<Handoff>;
|
|
59
63
|
/**
|
|
60
64
|
* Publish design token sets from this connected workspace to the configured remote registry. Runs a
|
|
61
65
|
* fresh token build (Figma extract + style transformers), discovers the logical sets, and uploads
|
package/dist/index.js
CHANGED
|
@@ -159,28 +159,44 @@ class Handoff {
|
|
|
159
159
|
});
|
|
160
160
|
}
|
|
161
161
|
/**
|
|
162
|
-
* Publish
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
162
|
+
* Publish components, patterns, or pages from this connected workspace to the configured remote
|
|
163
|
+
* registry. With `id` it publishes that one entity after a targeted build; without `id` it builds
|
|
164
|
+
* the kind once and publishes every declared entity, skipping ones whose content is unchanged on
|
|
165
|
+
* the registry (`--force` re-uploads all). Pages upload their record and markdown source because
|
|
166
|
+
* they render at runtime. The publish module is loaded lazily so the registry client and build code
|
|
167
|
+
* never enter the docs app bundle.
|
|
166
168
|
*/
|
|
167
169
|
publish(kind, id) {
|
|
168
170
|
return __awaiter(this, void 0, void 0, function* () {
|
|
169
171
|
this.preRunner();
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
if (id) {
|
|
173
|
+
const { publishEntity } = yield Promise.resolve().then(() => __importStar(require('./registry/publish')));
|
|
174
|
+
yield publishEntity(this, kind, id);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const { publishEntities } = yield Promise.resolve().then(() => __importStar(require('./registry/publish')));
|
|
178
|
+
yield publishEntities(this, kind);
|
|
179
|
+
}
|
|
172
180
|
return this;
|
|
173
181
|
});
|
|
174
182
|
}
|
|
175
183
|
/**
|
|
176
|
-
* Checkout
|
|
177
|
-
*
|
|
184
|
+
* Checkout components, patterns, or pages from the connected remote registry into this workspace.
|
|
185
|
+
* With `id` it checks out that one entity; without `id` it checks out every published entity of the
|
|
186
|
+
* kind. Overwriting existing local files requires `--force` or an interactive confirmation. The
|
|
187
|
+
* checkout module is loaded lazily so the registry client never enters the docs app bundle.
|
|
178
188
|
*/
|
|
179
189
|
checkout(kind, id) {
|
|
180
190
|
return __awaiter(this, void 0, void 0, function* () {
|
|
181
191
|
this.preRunner();
|
|
182
|
-
|
|
183
|
-
|
|
192
|
+
if (id) {
|
|
193
|
+
const { checkoutEntity } = yield Promise.resolve().then(() => __importStar(require('./registry/checkout')));
|
|
194
|
+
yield checkoutEntity(this, kind, id);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
const { checkoutEntities } = yield Promise.resolve().then(() => __importStar(require('./registry/checkout')));
|
|
198
|
+
yield checkoutEntities(this, kind);
|
|
199
|
+
}
|
|
184
200
|
return this;
|
|
185
201
|
});
|
|
186
202
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `runtime.registry.assetStorage` selects the active provider for **new** uploads: the built-in
|
|
5
5
|
* `database` inline default, the pre-packaged `vercel-blob` adapter, or a `custom` module. Mirrors
|
|
6
|
-
* {@link import('../db/
|
|
6
|
+
* {@link import('../db/driver')}: only provider selection, module location, non-secret options, and
|
|
7
7
|
* env-var *names* are ever persisted; secret *values* are read from `process.env` at request time.
|
|
8
8
|
*/
|
|
9
9
|
import type { Config } from '../../types/config';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* `runtime.registry.assetStorage` selects the active provider for **new** uploads: the built-in
|
|
6
6
|
* `database` inline default, the pre-packaged `vercel-blob` adapter, or a `custom` module. Mirrors
|
|
7
|
-
* {@link import('../db/
|
|
7
|
+
* {@link import('../db/driver')}: only provider selection, module location, non-secret options, and
|
|
8
8
|
* env-var *names* are ever persisted; secret *values* are read from `process.env` at request time.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -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>;
|
|
@@ -0,0 +1,178 @@
|
|
|
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.resetPassword = exports.createPasswordReset = exports.acceptUserInvitation = exports.resendUserInvitation = exports.createUserInvitation = exports.PASSWORD_RESET_LIFETIME_MS = exports.INVITATION_LIFETIME_MS = void 0;
|
|
13
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
14
|
+
const node_crypto_1 = require("node:crypto");
|
|
15
|
+
const schema_1 = require("../db/schema");
|
|
16
|
+
const access_tokens_1 = require("./access-tokens");
|
|
17
|
+
const crypto_1 = require("./crypto");
|
|
18
|
+
const database_1 = require("./database");
|
|
19
|
+
const password_1 = require("./password");
|
|
20
|
+
const user_record_1 = require("./user-record");
|
|
21
|
+
const validation_1 = require("./validation");
|
|
22
|
+
exports.INVITATION_LIFETIME_MS = 7 * 24 * 60 * 60 * 1000;
|
|
23
|
+
exports.PASSWORD_RESET_LIFETIME_MS = 60 * 60 * 1000;
|
|
24
|
+
const invalidateUnusedActionTokens = (db, userId, purpose, now) => __awaiter(void 0, void 0, void 0, function* () {
|
|
25
|
+
yield db
|
|
26
|
+
.update(schema_1.registryAuthActionTokens)
|
|
27
|
+
.set({ usedAt: now })
|
|
28
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryAuthActionTokens.userId, userId), (0, drizzle_orm_1.eq)(schema_1.registryAuthActionTokens.purpose, purpose), (0, drizzle_orm_1.isNull)(schema_1.registryAuthActionTokens.usedAt)));
|
|
29
|
+
});
|
|
30
|
+
const insertActionToken = (db, userId, purpose, expiresAt, now) => __awaiter(void 0, void 0, void 0, function* () {
|
|
31
|
+
const token = (0, crypto_1.createOpaqueSecret)(32);
|
|
32
|
+
yield db.insert(schema_1.registryAuthActionTokens).values({
|
|
33
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
34
|
+
userId,
|
|
35
|
+
purpose,
|
|
36
|
+
tokenHash: (0, crypto_1.hashSecret)(token),
|
|
37
|
+
expiresAt,
|
|
38
|
+
createdAt: now,
|
|
39
|
+
});
|
|
40
|
+
return token;
|
|
41
|
+
});
|
|
42
|
+
const createUserInvitation = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
|
|
43
|
+
var _a, _b;
|
|
44
|
+
const email = (0, validation_1.validateRegistryEmail)(input.email);
|
|
45
|
+
if (!email)
|
|
46
|
+
return { ok: false, reason: 'invalid_email' };
|
|
47
|
+
const name = (0, validation_1.validateRegistryDisplayName)(input.name);
|
|
48
|
+
if (!name)
|
|
49
|
+
return { ok: false, reason: 'invalid_name' };
|
|
50
|
+
if (input.role !== 'admin' && input.role !== 'member')
|
|
51
|
+
return { ok: false, reason: 'invalid_role' };
|
|
52
|
+
const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
|
|
53
|
+
const expiresAt = (_b = input.expiresAt) !== null && _b !== void 0 ? _b : new Date(now.getTime() + exports.INVITATION_LIFETIME_MS);
|
|
54
|
+
return (0, database_1.withRegistryTransaction)(db, (tx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
55
|
+
const userId = (0, node_crypto_1.randomUUID)();
|
|
56
|
+
const rows = yield tx
|
|
57
|
+
.insert(schema_1.registryUsers)
|
|
58
|
+
.values({
|
|
59
|
+
id: userId,
|
|
60
|
+
email,
|
|
61
|
+
name,
|
|
62
|
+
role: input.role,
|
|
63
|
+
status: 'invited',
|
|
64
|
+
createdAt: now,
|
|
65
|
+
updatedAt: now,
|
|
66
|
+
})
|
|
67
|
+
.onConflictDoNothing()
|
|
68
|
+
.returning();
|
|
69
|
+
const user = rows[0];
|
|
70
|
+
if (!user)
|
|
71
|
+
return { ok: false, reason: 'email_exists' };
|
|
72
|
+
const token = yield insertActionToken(tx, userId, 'invite', expiresAt, now);
|
|
73
|
+
return { ok: true, user: (0, user_record_1.toRegistryUser)(user), token, expiresAt };
|
|
74
|
+
}));
|
|
75
|
+
});
|
|
76
|
+
exports.createUserInvitation = createUserInvitation;
|
|
77
|
+
const resendUserInvitation = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
|
|
78
|
+
return (0, database_1.withRegistryTransaction)(db, (tx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
79
|
+
var _a, _b;
|
|
80
|
+
const [user] = yield tx.select().from(schema_1.registryUsers).where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, input.userId)).limit(1).for('update');
|
|
81
|
+
if (!user)
|
|
82
|
+
return { ok: false, reason: 'not_found' };
|
|
83
|
+
if (user.status === 'active')
|
|
84
|
+
return { ok: false, reason: 'already_accepted' };
|
|
85
|
+
if (user.status === 'deactivated')
|
|
86
|
+
return { ok: false, reason: 'deactivated' };
|
|
87
|
+
const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
|
|
88
|
+
const expiresAt = (_b = input.expiresAt) !== null && _b !== void 0 ? _b : new Date(now.getTime() + exports.INVITATION_LIFETIME_MS);
|
|
89
|
+
yield invalidateUnusedActionTokens(tx, user.id, 'invite', now);
|
|
90
|
+
const token = yield insertActionToken(tx, user.id, 'invite', expiresAt, now);
|
|
91
|
+
return { ok: true, user: (0, user_record_1.toRegistryUser)(user), token, expiresAt };
|
|
92
|
+
}));
|
|
93
|
+
});
|
|
94
|
+
exports.resendUserInvitation = resendUserInvitation;
|
|
95
|
+
const acceptUserInvitation = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
|
|
96
|
+
var _a;
|
|
97
|
+
const validation = (0, password_1.validatePassword)(input.password);
|
|
98
|
+
if ('error' in validation)
|
|
99
|
+
return { ok: false, reason: 'invalid_password', error: validation.error };
|
|
100
|
+
const passwordHash = yield (0, password_1.hashPassword)(input.password);
|
|
101
|
+
const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
|
|
102
|
+
return (0, database_1.withRegistryTransaction)(db, (tx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
103
|
+
const [consumed] = yield tx
|
|
104
|
+
.update(schema_1.registryAuthActionTokens)
|
|
105
|
+
.set({ usedAt: now })
|
|
106
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryAuthActionTokens.tokenHash, (0, crypto_1.hashSecret)(input.token)), (0, drizzle_orm_1.eq)(schema_1.registryAuthActionTokens.purpose, 'invite'), (0, drizzle_orm_1.isNull)(schema_1.registryAuthActionTokens.usedAt), (0, drizzle_orm_1.gt)(schema_1.registryAuthActionTokens.expiresAt, now)))
|
|
107
|
+
.returning({ userId: schema_1.registryAuthActionTokens.userId });
|
|
108
|
+
if (!consumed)
|
|
109
|
+
return { ok: false, reason: 'invalid_or_expired_token' };
|
|
110
|
+
const [user] = yield tx
|
|
111
|
+
.update(schema_1.registryUsers)
|
|
112
|
+
.set({
|
|
113
|
+
passwordHash,
|
|
114
|
+
status: 'active',
|
|
115
|
+
emailVerifiedAt: now,
|
|
116
|
+
updatedAt: now,
|
|
117
|
+
})
|
|
118
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, consumed.userId), (0, drizzle_orm_1.eq)(schema_1.registryUsers.status, 'invited')))
|
|
119
|
+
.returning();
|
|
120
|
+
return user ? { ok: true, user: (0, user_record_1.toRegistryUser)(user) } : { ok: false, reason: 'invalid_or_expired_token' };
|
|
121
|
+
}));
|
|
122
|
+
});
|
|
123
|
+
exports.acceptUserInvitation = acceptUserInvitation;
|
|
124
|
+
/**
|
|
125
|
+
* Enumeration-safe reset request. The token is returned only to the trusted mail-delivery caller;
|
|
126
|
+
* unknown/inactive emails return the same successful shape with a null token.
|
|
127
|
+
*/
|
|
128
|
+
const createPasswordReset = (db_1, email_1, ...args_1) => __awaiter(void 0, [db_1, email_1, ...args_1], void 0, function* (db, email, options = {}) {
|
|
129
|
+
var _a, _b;
|
|
130
|
+
const normalizedEmail = (0, crypto_1.normalizeEmail)(email);
|
|
131
|
+
const now = (_a = options.now) !== null && _a !== void 0 ? _a : new Date();
|
|
132
|
+
const expiresAt = (_b = options.expiresAt) !== null && _b !== void 0 ? _b : new Date(now.getTime() + exports.PASSWORD_RESET_LIFETIME_MS);
|
|
133
|
+
return (0, database_1.withRegistryTransaction)(db, (tx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
134
|
+
const [user] = yield tx
|
|
135
|
+
.select()
|
|
136
|
+
.from(schema_1.registryUsers)
|
|
137
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryUsers.email, normalizedEmail), (0, drizzle_orm_1.eq)(schema_1.registryUsers.status, 'active')))
|
|
138
|
+
.limit(1)
|
|
139
|
+
.for('update');
|
|
140
|
+
if (!user)
|
|
141
|
+
return { ok: true, user: null, token: null, expiresAt: null };
|
|
142
|
+
yield invalidateUnusedActionTokens(tx, user.id, 'password_reset', now);
|
|
143
|
+
const token = yield insertActionToken(tx, user.id, 'password_reset', expiresAt, now);
|
|
144
|
+
return { ok: true, user: (0, user_record_1.toRegistryUser)(user), token, expiresAt };
|
|
145
|
+
}));
|
|
146
|
+
});
|
|
147
|
+
exports.createPasswordReset = createPasswordReset;
|
|
148
|
+
const resetPassword = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
|
|
149
|
+
var _a;
|
|
150
|
+
const validation = (0, password_1.validatePassword)(input.password);
|
|
151
|
+
if ('error' in validation)
|
|
152
|
+
return { ok: false, reason: 'invalid_password', error: validation.error };
|
|
153
|
+
const passwordHash = yield (0, password_1.hashPassword)(input.password);
|
|
154
|
+
const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
|
|
155
|
+
return (0, database_1.withRegistryTransaction)(db, (tx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
156
|
+
const [consumed] = yield tx
|
|
157
|
+
.update(schema_1.registryAuthActionTokens)
|
|
158
|
+
.set({ usedAt: now })
|
|
159
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryAuthActionTokens.tokenHash, (0, crypto_1.hashSecret)(input.token)), (0, drizzle_orm_1.eq)(schema_1.registryAuthActionTokens.purpose, 'password_reset'), (0, drizzle_orm_1.isNull)(schema_1.registryAuthActionTokens.usedAt), (0, drizzle_orm_1.gt)(schema_1.registryAuthActionTokens.expiresAt, now)))
|
|
160
|
+
.returning({ userId: schema_1.registryAuthActionTokens.userId });
|
|
161
|
+
if (!consumed)
|
|
162
|
+
return { ok: false, reason: 'invalid_or_expired_token' };
|
|
163
|
+
const [user] = yield tx
|
|
164
|
+
.update(schema_1.registryUsers)
|
|
165
|
+
.set({
|
|
166
|
+
passwordHash,
|
|
167
|
+
authVersion: (0, drizzle_orm_1.sql) `${schema_1.registryUsers.authVersion} + 1`,
|
|
168
|
+
updatedAt: now,
|
|
169
|
+
})
|
|
170
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, consumed.userId), (0, drizzle_orm_1.eq)(schema_1.registryUsers.status, 'active')))
|
|
171
|
+
.returning();
|
|
172
|
+
if (!user)
|
|
173
|
+
return { ok: false, reason: 'invalid_or_expired_token' };
|
|
174
|
+
yield (0, access_tokens_1.revokeAllRegistryAccessTokens)(tx, user.id, now);
|
|
175
|
+
return { ok: true, user: (0, user_record_1.toRegistryUser)(user) };
|
|
176
|
+
}));
|
|
177
|
+
});
|
|
178
|
+
exports.resetPassword = resetPassword;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const hashSecret: (secret: string) => string;
|
|
2
|
+
export declare const createOpaqueSecret: (byteLength?: number) => string;
|
|
3
|
+
export declare const secretHashMatches: (secret: string, expectedHash: string) => boolean;
|
|
4
|
+
export declare const normalizeEmail: (email: string) => string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeEmail = exports.secretHashMatches = exports.createOpaqueSecret = exports.hashSecret = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const hashSecret = (secret) => (0, node_crypto_1.createHash)('sha256').update(secret, 'utf8').digest('hex');
|
|
6
|
+
exports.hashSecret = hashSecret;
|
|
7
|
+
const createOpaqueSecret = (byteLength = 32) => (0, node_crypto_1.randomBytes)(byteLength).toString('base64url');
|
|
8
|
+
exports.createOpaqueSecret = createOpaqueSecret;
|
|
9
|
+
const secretHashMatches = (secret, expectedHash) => {
|
|
10
|
+
const actual = Buffer.from((0, exports.hashSecret)(secret), 'hex');
|
|
11
|
+
const expected = Buffer.from(expectedHash, 'hex');
|
|
12
|
+
return actual.length === expected.length && (0, node_crypto_1.timingSafeEqual)(actual, expected);
|
|
13
|
+
};
|
|
14
|
+
exports.secretHashMatches = secretHashMatches;
|
|
15
|
+
const normalizeEmail = (email) => email.trim().toLowerCase();
|
|
16
|
+
exports.normalizeEmail = normalizeEmail;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RegistryDatabase } from '../db/client';
|
|
2
|
+
export type RegistryTransaction = RegistryDatabase;
|
|
3
|
+
export declare const withRegistryTransaction: <T>(db: RegistryDatabase, callback: (tx: RegistryTransaction) => Promise<T>) => Promise<T>;
|
|
4
|
+
export declare const acquireRegistryAdvisoryLock: (tx: RegistryTransaction, lockId: number) => Promise<void>;
|
|
5
|
+
/** Stable, package-owned Postgres advisory-lock ids. */
|
|
6
|
+
export declare const REGISTRY_INSTALL_LOCK_ID = 1841111001;
|
|
7
|
+
export declare const REGISTRY_ADMIN_LOCK_ID = 1841111002;
|
|
@@ -0,0 +1,22 @@
|
|
|
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.REGISTRY_ADMIN_LOCK_ID = exports.REGISTRY_INSTALL_LOCK_ID = exports.acquireRegistryAdvisoryLock = exports.withRegistryTransaction = void 0;
|
|
13
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
14
|
+
const withRegistryTransaction = (db, callback) => __awaiter(void 0, void 0, void 0, function* () { return db.transaction((tx) => callback(tx)); });
|
|
15
|
+
exports.withRegistryTransaction = withRegistryTransaction;
|
|
16
|
+
const acquireRegistryAdvisoryLock = (tx, lockId) => __awaiter(void 0, void 0, void 0, function* () {
|
|
17
|
+
yield tx.execute((0, drizzle_orm_1.sql) `select pg_advisory_xact_lock(${lockId})`);
|
|
18
|
+
});
|
|
19
|
+
exports.acquireRegistryAdvisoryLock = acquireRegistryAdvisoryLock;
|
|
20
|
+
/** Stable, package-owned Postgres advisory-lock ids. */
|
|
21
|
+
exports.REGISTRY_INSTALL_LOCK_ID = 1841111001;
|
|
22
|
+
exports.REGISTRY_ADMIN_LOCK_ID = 1841111002;
|