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
|
@@ -0,0 +1,75 @@
|
|
|
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.purgeExpiredAuthRateLimits = exports.clearAuthRateLimit = exports.consumeAuthRateLimit = exports.hashRateLimitIdentifier = 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 hashRateLimitIdentifier = (bucket, identifier) => (0, crypto_1.hashSecret)(`${bucket}:${identifier.trim().toLowerCase()}`);
|
|
17
|
+
exports.hashRateLimitIdentifier = hashRateLimitIdentifier;
|
|
18
|
+
/** Atomic fixed-window counter shared by all registry instances. */
|
|
19
|
+
const consumeAuthRateLimit = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
|
|
20
|
+
var _a;
|
|
21
|
+
if (!Number.isInteger(input.limit) || input.limit < 1)
|
|
22
|
+
throw new Error('Rate-limit limit must be a positive integer.');
|
|
23
|
+
if (!Number.isInteger(input.windowMs) || input.windowMs < 1) {
|
|
24
|
+
throw new Error('Rate-limit windowMs must be a positive integer.');
|
|
25
|
+
}
|
|
26
|
+
const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
|
|
27
|
+
const windowStartedAt = new Date(Math.floor(now.getTime() / input.windowMs) * input.windowMs);
|
|
28
|
+
const resetsAt = new Date(windowStartedAt.getTime() + input.windowMs);
|
|
29
|
+
const identifierHash = (0, exports.hashRateLimitIdentifier)(input.bucket, input.identifier);
|
|
30
|
+
const [counter] = yield db
|
|
31
|
+
.insert(schema_1.registryAuthRateLimits)
|
|
32
|
+
.values({
|
|
33
|
+
bucket: input.bucket,
|
|
34
|
+
identifierHash,
|
|
35
|
+
windowStartedAt,
|
|
36
|
+
attempts: 1,
|
|
37
|
+
expiresAt: resetsAt,
|
|
38
|
+
updatedAt: now,
|
|
39
|
+
})
|
|
40
|
+
.onConflictDoUpdate({
|
|
41
|
+
target: [schema_1.registryAuthRateLimits.bucket, schema_1.registryAuthRateLimits.identifierHash, schema_1.registryAuthRateLimits.windowStartedAt],
|
|
42
|
+
set: {
|
|
43
|
+
attempts: (0, drizzle_orm_1.sql) `${schema_1.registryAuthRateLimits.attempts} + 1`,
|
|
44
|
+
expiresAt: resetsAt,
|
|
45
|
+
updatedAt: now,
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
.returning({ attempts: schema_1.registryAuthRateLimits.attempts });
|
|
49
|
+
const attempts = counter.attempts;
|
|
50
|
+
return {
|
|
51
|
+
allowed: attempts <= input.limit,
|
|
52
|
+
attempts,
|
|
53
|
+
limit: input.limit,
|
|
54
|
+
remaining: Math.max(0, input.limit - attempts),
|
|
55
|
+
resetsAt,
|
|
56
|
+
retryAfterSeconds: attempts > input.limit ? Math.max(1, Math.ceil((resetsAt.getTime() - now.getTime()) / 1000)) : 0,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
exports.consumeAuthRateLimit = consumeAuthRateLimit;
|
|
60
|
+
const clearAuthRateLimit = (db, bucket, identifier) => __awaiter(void 0, void 0, void 0, function* () {
|
|
61
|
+
const rows = yield db
|
|
62
|
+
.delete(schema_1.registryAuthRateLimits)
|
|
63
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryAuthRateLimits.bucket, bucket), (0, drizzle_orm_1.eq)(schema_1.registryAuthRateLimits.identifierHash, (0, exports.hashRateLimitIdentifier)(bucket, identifier))))
|
|
64
|
+
.returning({ identifierHash: schema_1.registryAuthRateLimits.identifierHash });
|
|
65
|
+
return rows.length;
|
|
66
|
+
});
|
|
67
|
+
exports.clearAuthRateLimit = clearAuthRateLimit;
|
|
68
|
+
const purgeExpiredAuthRateLimits = (db_1, ...args_1) => __awaiter(void 0, [db_1, ...args_1], void 0, function* (db, now = new Date()) {
|
|
69
|
+
const rows = yield db
|
|
70
|
+
.delete(schema_1.registryAuthRateLimits)
|
|
71
|
+
.where((0, drizzle_orm_1.lt)(schema_1.registryAuthRateLimits.expiresAt, now))
|
|
72
|
+
.returning({ identifierHash: schema_1.registryAuthRateLimits.identifierHash });
|
|
73
|
+
return rows.length;
|
|
74
|
+
});
|
|
75
|
+
exports.purgeExpiredAuthRateLimits = purgeExpiredAuthRateLimits;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RegistryAccessScope, RegistryUserRole, RegistryUserStatus } from '../db/schema';
|
|
2
|
+
export type { RegistryAccessScope, RegistryUserRole, RegistryUserStatus };
|
|
3
|
+
export declare const REGISTRY_READ_SCOPE: "registry:read";
|
|
4
|
+
export declare const REGISTRY_WRITE_SCOPE: "registry:write";
|
|
5
|
+
export interface RegistryUser {
|
|
6
|
+
id: string;
|
|
7
|
+
email: string;
|
|
8
|
+
name: string | null;
|
|
9
|
+
image: string | null;
|
|
10
|
+
role: RegistryUserRole;
|
|
11
|
+
status: RegistryUserStatus;
|
|
12
|
+
emailVerifiedAt: Date | null;
|
|
13
|
+
authVersion: number;
|
|
14
|
+
createdAt: Date;
|
|
15
|
+
updatedAt: Date;
|
|
16
|
+
}
|
|
17
|
+
export interface RegistryPrincipal {
|
|
18
|
+
tokenId: string;
|
|
19
|
+
userId: string;
|
|
20
|
+
email: string;
|
|
21
|
+
name: string | null;
|
|
22
|
+
role: RegistryUserRole;
|
|
23
|
+
scopes: RegistryAccessScope[];
|
|
24
|
+
}
|
|
25
|
+
export declare const scopesForRegistryRole: (role: RegistryUserRole) => RegistryAccessScope[];
|
|
26
|
+
export declare const registryPrincipalHasScope: (principal: RegistryPrincipal, scope: RegistryAccessScope) => boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registryPrincipalHasScope = exports.scopesForRegistryRole = exports.REGISTRY_WRITE_SCOPE = exports.REGISTRY_READ_SCOPE = void 0;
|
|
4
|
+
exports.REGISTRY_READ_SCOPE = 'registry:read';
|
|
5
|
+
exports.REGISTRY_WRITE_SCOPE = 'registry:write';
|
|
6
|
+
const scopesForRegistryRole = (role) => role === 'admin' ? [exports.REGISTRY_READ_SCOPE, exports.REGISTRY_WRITE_SCOPE] : [exports.REGISTRY_READ_SCOPE];
|
|
7
|
+
exports.scopesForRegistryRole = scopesForRegistryRole;
|
|
8
|
+
const registryPrincipalHasScope = (principal, scope) => principal.scopes.includes(scope);
|
|
9
|
+
exports.registryPrincipalHasScope = registryPrincipalHasScope;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RegistryUser } from './types';
|
|
2
|
+
export declare const toRegistryUser: (row: {
|
|
3
|
+
id: string;
|
|
4
|
+
email: string;
|
|
5
|
+
name: string | null;
|
|
6
|
+
image: string | null;
|
|
7
|
+
role: RegistryUser["role"];
|
|
8
|
+
status: RegistryUser["status"];
|
|
9
|
+
emailVerifiedAt: Date | null;
|
|
10
|
+
authVersion: number;
|
|
11
|
+
createdAt: Date;
|
|
12
|
+
updatedAt: Date;
|
|
13
|
+
}) => RegistryUser;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toRegistryUser = void 0;
|
|
4
|
+
const toRegistryUser = (row) => ({
|
|
5
|
+
id: row.id,
|
|
6
|
+
email: row.email,
|
|
7
|
+
name: row.name,
|
|
8
|
+
image: row.image,
|
|
9
|
+
role: row.role,
|
|
10
|
+
status: row.status,
|
|
11
|
+
emailVerifiedAt: row.emailVerifiedAt,
|
|
12
|
+
authVersion: row.authVersion,
|
|
13
|
+
createdAt: row.createdAt,
|
|
14
|
+
updatedAt: row.updatedAt,
|
|
15
|
+
});
|
|
16
|
+
exports.toRegistryUser = toRegistryUser;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RegistryDatabase } from '../db/client';
|
|
2
|
+
import { type RegistryUserRole } from '../db/schema';
|
|
3
|
+
import type { RegistryUser, RegistryUserStatus } from './types';
|
|
4
|
+
export type RegistryUserMutationResult = {
|
|
5
|
+
ok: true;
|
|
6
|
+
user: RegistryUser;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
reason: 'not_found' | 'invalid_name' | 'invalid_image' | 'invalid_role' | 'last_admin' | 'self_deactivation' | 'invalid_status';
|
|
10
|
+
};
|
|
11
|
+
export declare const getRegistryUserById: (db: RegistryDatabase, userId: string) => Promise<RegistryUser | null>;
|
|
12
|
+
export declare const listRegistryUsers: (db: RegistryDatabase) => Promise<RegistryUser[]>;
|
|
13
|
+
/** Credentials authentication is intentionally enumeration-safe: all failures return null. */
|
|
14
|
+
export declare const authenticateRegistryCredentials: (db: RegistryDatabase, email: string, password: string) => Promise<RegistryUser | null>;
|
|
15
|
+
export declare const updateRegistryUserProfile: (db: RegistryDatabase, input: {
|
|
16
|
+
userId: string;
|
|
17
|
+
name: string;
|
|
18
|
+
image?: string | null;
|
|
19
|
+
now?: Date;
|
|
20
|
+
}) => Promise<RegistryUserMutationResult>;
|
|
21
|
+
export declare const updateRegistryUserRole: (db: RegistryDatabase, input: {
|
|
22
|
+
userId: string;
|
|
23
|
+
role: RegistryUserRole;
|
|
24
|
+
now?: Date;
|
|
25
|
+
}) => Promise<RegistryUserMutationResult>;
|
|
26
|
+
export declare const setRegistryUserStatus: (db: RegistryDatabase, input: {
|
|
27
|
+
userId: string;
|
|
28
|
+
status: Extract<RegistryUserStatus, "active" | "deactivated">;
|
|
29
|
+
actorUserId?: string;
|
|
30
|
+
now?: Date;
|
|
31
|
+
}) => Promise<RegistryUserMutationResult>;
|
|
@@ -0,0 +1,135 @@
|
|
|
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.setRegistryUserStatus = exports.updateRegistryUserRole = exports.updateRegistryUserProfile = exports.authenticateRegistryCredentials = exports.listRegistryUsers = exports.getRegistryUserById = void 0;
|
|
13
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
14
|
+
const schema_1 = require("../db/schema");
|
|
15
|
+
const access_tokens_1 = require("./access-tokens");
|
|
16
|
+
const database_1 = require("./database");
|
|
17
|
+
const crypto_1 = require("./crypto");
|
|
18
|
+
const password_1 = require("./password");
|
|
19
|
+
const user_record_1 = require("./user-record");
|
|
20
|
+
const validation_1 = require("./validation");
|
|
21
|
+
const DUMMY_PASSWORD_HASH = 'scrypt-v1:AAAAAAAAAAAAAAAAAAAAAA:yK99vlYIaV57oeErq8gHqx18DxRfiUoHwETrTFUjt5JasWeMsu62stPgY-fO0TUeUc6uAu1hge_vuSQY15vvuQ';
|
|
22
|
+
const getRegistryUserById = (db, userId) => __awaiter(void 0, void 0, void 0, function* () {
|
|
23
|
+
const [row] = yield db.select().from(schema_1.registryUsers).where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, userId)).limit(1);
|
|
24
|
+
return row ? (0, user_record_1.toRegistryUser)(row) : null;
|
|
25
|
+
});
|
|
26
|
+
exports.getRegistryUserById = getRegistryUserById;
|
|
27
|
+
const listRegistryUsers = (db) => __awaiter(void 0, void 0, void 0, function* () {
|
|
28
|
+
const rows = yield db.select().from(schema_1.registryUsers).orderBy(schema_1.registryUsers.createdAt);
|
|
29
|
+
return rows.map(user_record_1.toRegistryUser);
|
|
30
|
+
});
|
|
31
|
+
exports.listRegistryUsers = listRegistryUsers;
|
|
32
|
+
/** Credentials authentication is intentionally enumeration-safe: all failures return null. */
|
|
33
|
+
const authenticateRegistryCredentials = (db, email, password) => __awaiter(void 0, void 0, void 0, function* () {
|
|
34
|
+
var _a;
|
|
35
|
+
const normalizedEmail = (0, crypto_1.normalizeEmail)(email);
|
|
36
|
+
const [row] = yield db.select().from(schema_1.registryUsers).where((0, drizzle_orm_1.eq)(schema_1.registryUsers.email, normalizedEmail)).limit(1);
|
|
37
|
+
const passwordMatches = yield (0, password_1.verifyPassword)(password, (_a = row === null || row === void 0 ? void 0 : row.passwordHash) !== null && _a !== void 0 ? _a : DUMMY_PASSWORD_HASH);
|
|
38
|
+
return row && row.status === 'active' && row.passwordHash && passwordMatches ? (0, user_record_1.toRegistryUser)(row) : null;
|
|
39
|
+
});
|
|
40
|
+
exports.authenticateRegistryCredentials = authenticateRegistryCredentials;
|
|
41
|
+
const updateRegistryUserProfile = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
|
|
42
|
+
var _a;
|
|
43
|
+
const name = (0, validation_1.validateRegistryDisplayName)(input.name);
|
|
44
|
+
if (!name)
|
|
45
|
+
return { ok: false, reason: 'invalid_name' };
|
|
46
|
+
const image = (0, validation_1.validateRegistryImageUrl)(input.image);
|
|
47
|
+
if (image === false)
|
|
48
|
+
return { ok: false, reason: 'invalid_image' };
|
|
49
|
+
const [row] = yield db
|
|
50
|
+
.update(schema_1.registryUsers)
|
|
51
|
+
.set({ name, image, updatedAt: (_a = input.now) !== null && _a !== void 0 ? _a : new Date() })
|
|
52
|
+
.where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, input.userId))
|
|
53
|
+
.returning();
|
|
54
|
+
return row ? { ok: true, user: (0, user_record_1.toRegistryUser)(row) } : { ok: false, reason: 'not_found' };
|
|
55
|
+
});
|
|
56
|
+
exports.updateRegistryUserProfile = updateRegistryUserProfile;
|
|
57
|
+
const updateRegistryUserRole = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
|
|
58
|
+
if (input.role !== 'admin' && input.role !== 'member')
|
|
59
|
+
return { ok: false, reason: 'invalid_role' };
|
|
60
|
+
return (0, database_1.withRegistryTransaction)(db, (tx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
61
|
+
var _a;
|
|
62
|
+
yield (0, database_1.acquireRegistryAdvisoryLock)(tx, database_1.REGISTRY_ADMIN_LOCK_ID);
|
|
63
|
+
const [target] = yield tx.select().from(schema_1.registryUsers).where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, input.userId)).limit(1);
|
|
64
|
+
if (!target)
|
|
65
|
+
return { ok: false, reason: 'not_found' };
|
|
66
|
+
if (target.role === input.role)
|
|
67
|
+
return { ok: true, user: (0, user_record_1.toRegistryUser)(target) };
|
|
68
|
+
if (target.role === 'admin' && target.status === 'active' && input.role === 'member') {
|
|
69
|
+
const [{ value }] = yield tx
|
|
70
|
+
.select({ value: (0, drizzle_orm_1.count)() })
|
|
71
|
+
.from(schema_1.registryUsers)
|
|
72
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryUsers.role, 'admin'), (0, drizzle_orm_1.eq)(schema_1.registryUsers.status, 'active')));
|
|
73
|
+
if (Number(value) <= 1)
|
|
74
|
+
return { ok: false, reason: 'last_admin' };
|
|
75
|
+
}
|
|
76
|
+
const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
|
|
77
|
+
const [row] = yield tx
|
|
78
|
+
.update(schema_1.registryUsers)
|
|
79
|
+
.set({
|
|
80
|
+
role: input.role,
|
|
81
|
+
authVersion: (0, drizzle_orm_1.sql) `${schema_1.registryUsers.authVersion} + 1`,
|
|
82
|
+
updatedAt: now,
|
|
83
|
+
})
|
|
84
|
+
.where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, input.userId))
|
|
85
|
+
.returning();
|
|
86
|
+
if (target.role === 'admin' && input.role === 'member') {
|
|
87
|
+
yield (0, access_tokens_1.revokeAllRegistryAccessTokens)(tx, input.userId, now);
|
|
88
|
+
}
|
|
89
|
+
return { ok: true, user: (0, user_record_1.toRegistryUser)(row) };
|
|
90
|
+
}));
|
|
91
|
+
});
|
|
92
|
+
exports.updateRegistryUserRole = updateRegistryUserRole;
|
|
93
|
+
const setRegistryUserStatus = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
|
|
94
|
+
if (input.status !== 'active' && input.status !== 'deactivated') {
|
|
95
|
+
return { ok: false, reason: 'invalid_status' };
|
|
96
|
+
}
|
|
97
|
+
if (input.status === 'deactivated' && input.actorUserId === input.userId) {
|
|
98
|
+
return { ok: false, reason: 'self_deactivation' };
|
|
99
|
+
}
|
|
100
|
+
return (0, database_1.withRegistryTransaction)(db, (tx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
101
|
+
var _a;
|
|
102
|
+
yield (0, database_1.acquireRegistryAdvisoryLock)(tx, database_1.REGISTRY_ADMIN_LOCK_ID);
|
|
103
|
+
const [target] = yield tx.select().from(schema_1.registryUsers).where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, input.userId)).limit(1);
|
|
104
|
+
if (!target)
|
|
105
|
+
return { ok: false, reason: 'not_found' };
|
|
106
|
+
if (target.status === input.status)
|
|
107
|
+
return { ok: true, user: (0, user_record_1.toRegistryUser)(target) };
|
|
108
|
+
if (input.status === 'active' && target.status === 'invited') {
|
|
109
|
+
return { ok: false, reason: 'invalid_status' };
|
|
110
|
+
}
|
|
111
|
+
if (target.role === 'admin' && target.status === 'active' && input.status === 'deactivated') {
|
|
112
|
+
const [{ value }] = yield tx
|
|
113
|
+
.select({ value: (0, drizzle_orm_1.count)() })
|
|
114
|
+
.from(schema_1.registryUsers)
|
|
115
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryUsers.role, 'admin'), (0, drizzle_orm_1.eq)(schema_1.registryUsers.status, 'active')));
|
|
116
|
+
if (Number(value) <= 1)
|
|
117
|
+
return { ok: false, reason: 'last_admin' };
|
|
118
|
+
}
|
|
119
|
+
const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
|
|
120
|
+
const [row] = yield tx
|
|
121
|
+
.update(schema_1.registryUsers)
|
|
122
|
+
.set({
|
|
123
|
+
status: input.status,
|
|
124
|
+
authVersion: (0, drizzle_orm_1.sql) `${schema_1.registryUsers.authVersion} + 1`,
|
|
125
|
+
updatedAt: now,
|
|
126
|
+
})
|
|
127
|
+
.where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, input.userId))
|
|
128
|
+
.returning();
|
|
129
|
+
if (input.status === 'deactivated') {
|
|
130
|
+
yield (0, access_tokens_1.revokeAllRegistryAccessTokens)(tx, input.userId, now);
|
|
131
|
+
}
|
|
132
|
+
return { ok: true, user: (0, user_record_1.toRegistryUser)(row) };
|
|
133
|
+
}));
|
|
134
|
+
});
|
|
135
|
+
exports.setRegistryUserStatus = setRegistryUserStatus;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateRegistryImageUrl = exports.validateRegistryDisplayName = exports.validateRegistryEmail = void 0;
|
|
4
|
+
const crypto_1 = require("./crypto");
|
|
5
|
+
const SIMPLE_EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6
|
+
const validateRegistryEmail = (email) => {
|
|
7
|
+
const normalized = (0, crypto_1.normalizeEmail)(email);
|
|
8
|
+
return normalized.length <= 320 && SIMPLE_EMAIL_PATTERN.test(normalized) ? normalized : null;
|
|
9
|
+
};
|
|
10
|
+
exports.validateRegistryEmail = validateRegistryEmail;
|
|
11
|
+
const validateRegistryDisplayName = (name) => {
|
|
12
|
+
const normalized = name.trim();
|
|
13
|
+
return normalized && normalized.length <= 120 ? normalized : null;
|
|
14
|
+
};
|
|
15
|
+
exports.validateRegistryDisplayName = validateRegistryDisplayName;
|
|
16
|
+
const validateRegistryImageUrl = (image) => {
|
|
17
|
+
const normalized = image === null || image === void 0 ? void 0 : image.trim();
|
|
18
|
+
if (!normalized)
|
|
19
|
+
return null;
|
|
20
|
+
try {
|
|
21
|
+
const parsed = new URL(normalized);
|
|
22
|
+
return parsed.protocol === 'https:' && normalized.length <= 2048 ? normalized : false;
|
|
23
|
+
}
|
|
24
|
+
catch (_a) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
exports.validateRegistryImageUrl = validateRegistryImageUrl;
|
|
@@ -62,7 +62,9 @@ const describeFetchFailure = (error, target, registryUrl) => {
|
|
|
62
62
|
case 'runtime_mode_conflict':
|
|
63
63
|
return `The registry at ${registryUrl} is not running in registry mode, so it cannot serve a checkout: ${error.message}`;
|
|
64
64
|
case 'unauthorized':
|
|
65
|
-
return `The registry rejected the access token (401).
|
|
65
|
+
return `The registry rejected the access token (401). Run \`handoff-app login --url ${registryUrl}\` again, or replace the user-issued CI token.`;
|
|
66
|
+
case 'forbidden':
|
|
67
|
+
return `The registry token does not have permission to checkout assets (403). Authorize a token with registry:read access.`;
|
|
66
68
|
default:
|
|
67
69
|
return error.message;
|
|
68
70
|
}
|
|
@@ -83,7 +85,7 @@ const wouldChange = (absolutePath, bytes) => {
|
|
|
83
85
|
* Recreates workspace asset files; prompts before overwriting changed local files unless `--force`.
|
|
84
86
|
*/
|
|
85
87
|
const checkoutAssets = (handoff, collection) => __awaiter(void 0, void 0, void 0, function* () {
|
|
86
|
-
const connection = (0, index_1.resolveConnectionOrThrow)(handoff);
|
|
88
|
+
const connection = yield (0, index_1.resolveConnectionOrThrow)(handoff);
|
|
87
89
|
if (collection && !(0, sets_1.isAssetCollection)(collection)) {
|
|
88
90
|
throw new index_1.CheckoutError(`Unknown asset collection "${collection}". Supported collections: ${sets_1.ASSET_COLLECTIONS.join(', ')}.`);
|
|
89
91
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Connected-workspace checkout orchestration.
|
|
3
3
|
*
|
|
4
|
-
* `checkout <
|
|
4
|
+
* `checkout <components|patterns> <id>` reads a normalized registry record and its registry-safe
|
|
5
5
|
* source files from the connected registry through the shared registry client, then writes them
|
|
6
6
|
* into the local workspace in standard authoring form. The local **declaration is synthesized
|
|
7
7
|
* locally** in the configured `runtime.workspace.declarationFormat` — declarations are never read
|
|
@@ -23,11 +23,16 @@ export declare class CheckoutError extends Error {
|
|
|
23
23
|
* host has no local workspace to write into) and a resolved registry URL + access token. Throws an
|
|
24
24
|
* actionable {@link CheckoutError} naming the exact misconfiguration.
|
|
25
25
|
*/
|
|
26
|
-
export declare const resolveConnectionOrThrow: (handoff: Handoff) => import("../connection").ResolvedRegistryConnection
|
|
26
|
+
export declare const resolveConnectionOrThrow: (handoff: Handoff) => Promise<import("../connection").ResolvedRegistryConnection>;
|
|
27
27
|
/**
|
|
28
|
-
* Checkout a single component or
|
|
29
|
-
*
|
|
30
|
-
* source files and synthesize the local declaration. Throws {@link CheckoutError} with actionable
|
|
31
|
-
* messaging on any precondition or fetch failure.
|
|
28
|
+
* Checkout a single component, pattern, or page from the connected registry into this workspace.
|
|
29
|
+
* Overwriting existing local files requires `--force` or an interactive confirmation.
|
|
32
30
|
*/
|
|
33
31
|
export declare const checkoutEntity: (handoff: Handoff, kind: TransferEntityKind, id: string) => Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Checkout every published component, pattern, or page of the kind into this workspace. Enumerates
|
|
34
|
+
* the registry's published ids, then checks each out through the shared client. A per-entity failure
|
|
35
|
+
* is collected and never aborts the rest; the run throws at the end if any entity failed. Overwrite
|
|
36
|
+
* prompting is per entity (skipped under `--force`).
|
|
37
|
+
*/
|
|
38
|
+
export declare const checkoutEntities: (handoff: Handoff, kind: TransferEntityKind) => Promise<void>;
|