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,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;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { RegistryDatabase } from '../db/client';
|
|
2
|
+
import { type RegistryAccessTokenSummary } from './access-tokens';
|
|
3
|
+
import { type RegistryAccessScope, type RegistryUser } from './types';
|
|
4
|
+
export declare const DEVICE_AUTHORIZATION_LIFETIME_SECONDS: number;
|
|
5
|
+
export declare const DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS = 5;
|
|
6
|
+
export declare const generateRegistryUserCode: () => string;
|
|
7
|
+
export declare const normalizeRegistryUserCode: (value: string) => string;
|
|
8
|
+
export interface CreateRegistryDeviceAuthorizationResult {
|
|
9
|
+
deviceCode: string;
|
|
10
|
+
userCode: string;
|
|
11
|
+
expiresIn: number;
|
|
12
|
+
interval: number;
|
|
13
|
+
expiresAt: Date;
|
|
14
|
+
}
|
|
15
|
+
export declare const createRegistryDeviceAuthorization: (db: RegistryDatabase, options?: {
|
|
16
|
+
now?: Date;
|
|
17
|
+
expiresInSeconds?: number;
|
|
18
|
+
}) => Promise<CreateRegistryDeviceAuthorizationResult>;
|
|
19
|
+
export type RegistryDeviceApprovalResult = {
|
|
20
|
+
ok: true;
|
|
21
|
+
user: RegistryUser;
|
|
22
|
+
scopes: RegistryAccessScope[];
|
|
23
|
+
} | {
|
|
24
|
+
ok: false;
|
|
25
|
+
reason: 'invalid_or_expired_code' | 'user_inactive';
|
|
26
|
+
};
|
|
27
|
+
export declare const approveRegistryDeviceAuthorization: (db: RegistryDatabase, input: {
|
|
28
|
+
userCode: string;
|
|
29
|
+
userId: string;
|
|
30
|
+
now?: Date;
|
|
31
|
+
}) => Promise<RegistryDeviceApprovalResult>;
|
|
32
|
+
export declare const denyRegistryDeviceAuthorization: (db: RegistryDatabase, userCode: string, now?: Date) => Promise<boolean>;
|
|
33
|
+
export type ExchangeRegistryDeviceAuthorizationResult = {
|
|
34
|
+
ok: true;
|
|
35
|
+
accessToken: string;
|
|
36
|
+
tokenType: 'Bearer';
|
|
37
|
+
expiresIn: number;
|
|
38
|
+
scopes: RegistryAccessScope[];
|
|
39
|
+
user: RegistryUser;
|
|
40
|
+
record: RegistryAccessTokenSummary;
|
|
41
|
+
} | {
|
|
42
|
+
ok: false;
|
|
43
|
+
error: 'invalid_grant' | 'expired_token' | 'authorization_pending' | 'access_denied';
|
|
44
|
+
errorDescription: string;
|
|
45
|
+
};
|
|
46
|
+
export declare const exchangeRegistryDeviceAuthorization: (db: RegistryDatabase, input: {
|
|
47
|
+
deviceCode: string;
|
|
48
|
+
tokenName?: string;
|
|
49
|
+
now?: Date;
|
|
50
|
+
}) => Promise<ExchangeRegistryDeviceAuthorizationResult>;
|
|
51
|
+
export declare const purgeExpiredRegistryDeviceAuthorizations: (db: RegistryDatabase, now?: Date) => Promise<number>;
|
|
@@ -0,0 +1,170 @@
|
|
|
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.purgeExpiredRegistryDeviceAuthorizations = exports.exchangeRegistryDeviceAuthorization = exports.denyRegistryDeviceAuthorization = exports.approveRegistryDeviceAuthorization = exports.createRegistryDeviceAuthorization = exports.normalizeRegistryUserCode = exports.generateRegistryUserCode = exports.DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS = exports.DEVICE_AUTHORIZATION_LIFETIME_SECONDS = 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 types_1 = require("./types");
|
|
20
|
+
const user_record_1 = require("./user-record");
|
|
21
|
+
const USER_CODE_ALPHABET = 'BCDFGHJKLMNPQRSTVWXYZ23456789';
|
|
22
|
+
exports.DEVICE_AUTHORIZATION_LIFETIME_SECONDS = 15 * 60;
|
|
23
|
+
exports.DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS = 5;
|
|
24
|
+
const randomUserCodeSegment = (length) => {
|
|
25
|
+
const bytes = (0, node_crypto_1.randomBytes)(length);
|
|
26
|
+
let result = '';
|
|
27
|
+
for (let index = 0; index < length; index += 1) {
|
|
28
|
+
result += USER_CODE_ALPHABET[bytes[index] % USER_CODE_ALPHABET.length];
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
const generateRegistryUserCode = () => `${randomUserCodeSegment(4)}-${randomUserCodeSegment(4)}`;
|
|
33
|
+
exports.generateRegistryUserCode = generateRegistryUserCode;
|
|
34
|
+
const normalizeRegistryUserCode = (value) => {
|
|
35
|
+
const compact = value.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
|
36
|
+
return compact.length === 8 ? `${compact.slice(0, 4)}-${compact.slice(4)}` : value.trim().toUpperCase();
|
|
37
|
+
};
|
|
38
|
+
exports.normalizeRegistryUserCode = normalizeRegistryUserCode;
|
|
39
|
+
const createRegistryDeviceAuthorization = (db_1, ...args_1) => __awaiter(void 0, [db_1, ...args_1], void 0, function* (db, options = {}) {
|
|
40
|
+
var _a, _b;
|
|
41
|
+
const now = (_a = options.now) !== null && _a !== void 0 ? _a : new Date();
|
|
42
|
+
const expiresIn = (_b = options.expiresInSeconds) !== null && _b !== void 0 ? _b : exports.DEVICE_AUTHORIZATION_LIFETIME_SECONDS;
|
|
43
|
+
const deviceCode = (0, node_crypto_1.randomBytes)(32).toString('base64url');
|
|
44
|
+
const userCode = (0, exports.generateRegistryUserCode)();
|
|
45
|
+
const expiresAt = new Date(now.getTime() + expiresIn * 1000);
|
|
46
|
+
yield db.insert(schema_1.registryDeviceAuthorizations).values({
|
|
47
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
48
|
+
deviceCodeHash: (0, crypto_1.hashSecret)(deviceCode),
|
|
49
|
+
userCode,
|
|
50
|
+
status: 'pending',
|
|
51
|
+
scopes: [],
|
|
52
|
+
expiresAt,
|
|
53
|
+
createdAt: now,
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
deviceCode,
|
|
57
|
+
userCode,
|
|
58
|
+
expiresIn,
|
|
59
|
+
interval: exports.DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS,
|
|
60
|
+
expiresAt,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
exports.createRegistryDeviceAuthorization = createRegistryDeviceAuthorization;
|
|
64
|
+
const approveRegistryDeviceAuthorization = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
|
|
65
|
+
var _a;
|
|
66
|
+
const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
|
|
67
|
+
const [user] = yield db.select().from(schema_1.registryUsers).where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, input.userId)).limit(1);
|
|
68
|
+
if (!user || user.status !== 'active')
|
|
69
|
+
return { ok: false, reason: 'user_inactive' };
|
|
70
|
+
const scopes = (0, types_1.scopesForRegistryRole)(user.role);
|
|
71
|
+
const [authorization] = yield db
|
|
72
|
+
.update(schema_1.registryDeviceAuthorizations)
|
|
73
|
+
.set({
|
|
74
|
+
status: 'approved',
|
|
75
|
+
userId: user.id,
|
|
76
|
+
scopes,
|
|
77
|
+
approvedAt: now,
|
|
78
|
+
})
|
|
79
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryDeviceAuthorizations.userCode, (0, exports.normalizeRegistryUserCode)(input.userCode)), (0, drizzle_orm_1.eq)(schema_1.registryDeviceAuthorizations.status, 'pending'), (0, drizzle_orm_1.gt)(schema_1.registryDeviceAuthorizations.expiresAt, now)))
|
|
80
|
+
.returning({ id: schema_1.registryDeviceAuthorizations.id });
|
|
81
|
+
return authorization ? { ok: true, user: (0, user_record_1.toRegistryUser)(user), scopes } : { ok: false, reason: 'invalid_or_expired_code' };
|
|
82
|
+
});
|
|
83
|
+
exports.approveRegistryDeviceAuthorization = approveRegistryDeviceAuthorization;
|
|
84
|
+
const denyRegistryDeviceAuthorization = (db_1, userCode_1, ...args_1) => __awaiter(void 0, [db_1, userCode_1, ...args_1], void 0, function* (db, userCode, now = new Date()) {
|
|
85
|
+
const rows = yield db
|
|
86
|
+
.update(schema_1.registryDeviceAuthorizations)
|
|
87
|
+
.set({ status: 'denied' })
|
|
88
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryDeviceAuthorizations.userCode, (0, exports.normalizeRegistryUserCode)(userCode)), (0, drizzle_orm_1.eq)(schema_1.registryDeviceAuthorizations.status, 'pending'), (0, drizzle_orm_1.gt)(schema_1.registryDeviceAuthorizations.expiresAt, now)))
|
|
89
|
+
.returning({ id: schema_1.registryDeviceAuthorizations.id });
|
|
90
|
+
return rows.length > 0;
|
|
91
|
+
});
|
|
92
|
+
exports.denyRegistryDeviceAuthorization = denyRegistryDeviceAuthorization;
|
|
93
|
+
const exchangeRegistryDeviceAuthorization = (db, input) => __awaiter(void 0, void 0, void 0, function* () {
|
|
94
|
+
var _a, _b;
|
|
95
|
+
const now = (_a = input.now) !== null && _a !== void 0 ? _a : new Date();
|
|
96
|
+
const tokenName = ((_b = input.tokenName) === null || _b === void 0 ? void 0 : _b.trim()) || 'Handoff CLI';
|
|
97
|
+
return (0, database_1.withRegistryTransaction)(db, (tx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
98
|
+
const [authorization] = yield tx
|
|
99
|
+
.select()
|
|
100
|
+
.from(schema_1.registryDeviceAuthorizations)
|
|
101
|
+
.where((0, drizzle_orm_1.eq)(schema_1.registryDeviceAuthorizations.deviceCodeHash, (0, crypto_1.hashSecret)(input.deviceCode)))
|
|
102
|
+
.limit(1);
|
|
103
|
+
if (!authorization) {
|
|
104
|
+
return { ok: false, error: 'invalid_grant', errorDescription: 'Unknown device code.' };
|
|
105
|
+
}
|
|
106
|
+
if (authorization.expiresAt <= now) {
|
|
107
|
+
return { ok: false, error: 'expired_token', errorDescription: 'Device authorization expired.' };
|
|
108
|
+
}
|
|
109
|
+
if (authorization.status === 'pending') {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: 'authorization_pending',
|
|
113
|
+
errorDescription: 'The authorization request is still pending.',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (authorization.status === 'denied') {
|
|
117
|
+
return { ok: false, error: 'access_denied', errorDescription: 'The authorization request was denied.' };
|
|
118
|
+
}
|
|
119
|
+
if (authorization.status === 'consumed') {
|
|
120
|
+
return { ok: false, error: 'invalid_grant', errorDescription: 'Device code has already been used.' };
|
|
121
|
+
}
|
|
122
|
+
if (!authorization.userId) {
|
|
123
|
+
return { ok: false, error: 'invalid_grant', errorDescription: 'Device authorization has no user.' };
|
|
124
|
+
}
|
|
125
|
+
const [consumed] = yield tx
|
|
126
|
+
.update(schema_1.registryDeviceAuthorizations)
|
|
127
|
+
.set({ status: 'consumed', consumedAt: now })
|
|
128
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.registryDeviceAuthorizations.id, authorization.id), (0, drizzle_orm_1.eq)(schema_1.registryDeviceAuthorizations.status, 'approved'), (0, drizzle_orm_1.gt)(schema_1.registryDeviceAuthorizations.expiresAt, now)))
|
|
129
|
+
.returning({ id: schema_1.registryDeviceAuthorizations.id });
|
|
130
|
+
if (!consumed) {
|
|
131
|
+
return { ok: false, error: 'invalid_grant', errorDescription: 'Device code has already been used.' };
|
|
132
|
+
}
|
|
133
|
+
const [user] = yield tx.select().from(schema_1.registryUsers).where((0, drizzle_orm_1.eq)(schema_1.registryUsers.id, authorization.userId)).limit(1);
|
|
134
|
+
if (!user || user.status !== 'active') {
|
|
135
|
+
return { ok: false, error: 'access_denied', errorDescription: 'The approving account is inactive.' };
|
|
136
|
+
}
|
|
137
|
+
let scopes = (0, access_tokens_1.normalizeRegistryScopes)(authorization.scopes);
|
|
138
|
+
if (!(0, access_tokens_1.registryRoleAllowsScopes)(user.role, scopes))
|
|
139
|
+
scopes = (0, types_1.scopesForRegistryRole)(user.role);
|
|
140
|
+
const expiresAt = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000);
|
|
141
|
+
const token = yield (0, access_tokens_1.createRegistryAccessToken)(tx, {
|
|
142
|
+
userId: user.id,
|
|
143
|
+
name: tokenName,
|
|
144
|
+
scopes,
|
|
145
|
+
expiresAt,
|
|
146
|
+
now,
|
|
147
|
+
});
|
|
148
|
+
if (!token.ok) {
|
|
149
|
+
return { ok: false, error: 'access_denied', errorDescription: 'The account cannot issue this token.' };
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
accessToken: token.token,
|
|
154
|
+
tokenType: 'Bearer',
|
|
155
|
+
expiresIn: Math.floor((expiresAt.getTime() - now.getTime()) / 1000),
|
|
156
|
+
scopes: token.record.scopes,
|
|
157
|
+
user: (0, user_record_1.toRegistryUser)(user),
|
|
158
|
+
record: token.record,
|
|
159
|
+
};
|
|
160
|
+
}));
|
|
161
|
+
});
|
|
162
|
+
exports.exchangeRegistryDeviceAuthorization = exchangeRegistryDeviceAuthorization;
|
|
163
|
+
const purgeExpiredRegistryDeviceAuthorizations = (db_1, ...args_1) => __awaiter(void 0, [db_1, ...args_1], void 0, function* (db, now = new Date()) {
|
|
164
|
+
const rows = yield db
|
|
165
|
+
.delete(schema_1.registryDeviceAuthorizations)
|
|
166
|
+
.where((0, drizzle_orm_1.lt)(schema_1.registryDeviceAuthorizations.expiresAt, now))
|
|
167
|
+
.returning({ id: schema_1.registryDeviceAuthorizations.id });
|
|
168
|
+
return rows.length;
|
|
169
|
+
});
|
|
170
|
+
exports.purgeExpiredRegistryDeviceAuthorizations = purgeExpiredRegistryDeviceAuthorizations;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const buildGravatarUrl: (email: string) => string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildGravatarUrl = void 0;
|
|
4
|
+
const crypto_1 = require("./crypto");
|
|
5
|
+
// Gravatar resolves an avatar from the SHA-256 hash of the trimmed, lowercased email.
|
|
6
|
+
// `d=identicon` guarantees a generated image renders even when the user has no Gravatar.
|
|
7
|
+
const buildGravatarUrl = (email) => `https://www.gravatar.com/avatar/${(0, crypto_1.hashSecret)((0, crypto_1.normalizeEmail)(email))}?d=identicon`;
|
|
8
|
+
exports.buildGravatarUrl = buildGravatarUrl;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { createRegistryAccessToken, listRegistryAccessTokens, revokeRegistryAccessToken, revokeAllRegistryAccessTokens, authenticateRegistryAccessToken, formatRegistryAccessToken, parseRegistryAccessToken, normalizeRegistryScopes, registryRoleAllowsScopes, type RegistryAccessTokenSummary, type CreateRegistryAccessTokenResult, } from './access-tokens';
|
|
2
|
+
export { createUserInvitation, resendUserInvitation, acceptUserInvitation, createPasswordReset, resetPassword, INVITATION_LIFETIME_MS, PASSWORD_RESET_LIFETIME_MS, type CreateUserInvitationResult, type ResendUserInvitationResult, type ConsumeAuthActionResult, } from './action-tokens';
|
|
3
|
+
export { createOpaqueSecret, hashSecret, normalizeEmail, secretHashMatches } from './crypto';
|
|
4
|
+
export { buildGravatarUrl } from './gravatar';
|
|
5
|
+
export { createRegistryDeviceAuthorization, approveRegistryDeviceAuthorization, denyRegistryDeviceAuthorization, exchangeRegistryDeviceAuthorization, purgeExpiredRegistryDeviceAuthorizations, generateRegistryUserCode, normalizeRegistryUserCode, DEVICE_AUTHORIZATION_LIFETIME_SECONDS, DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS, type CreateRegistryDeviceAuthorizationResult, type RegistryDeviceApprovalResult, type ExchangeRegistryDeviceAuthorizationResult, } from './device-authorizations';
|
|
6
|
+
export { getRegistryInstallation, getRegistryInstallationState, installRegistry, REGISTRY_AUTH_SCHEMA_VERSION, REGISTRY_INSTALLATION_ID, type RegistryInstallation, type RegistryInstallationState, type RegistryInstallInput, type RegistryInstallResult, } from './installation';
|
|
7
|
+
export { hashPassword, verifyPassword, validatePassword, MINIMUM_PASSWORD_LENGTH, MAXIMUM_PASSWORD_LENGTH, type PasswordValidationResult, } from './password';
|
|
8
|
+
export { consumeAuthRateLimit, clearAuthRateLimit, purgeExpiredAuthRateLimits, hashRateLimitIdentifier, type ConsumeAuthRateLimitInput, type AuthRateLimitResult, } from './rate-limits';
|
|
9
|
+
export { getRegistryUserById, listRegistryUsers, authenticateRegistryCredentials, updateRegistryUserProfile, updateRegistryUserRole, setRegistryUserStatus, type RegistryUserMutationResult, } from './users';
|
|
10
|
+
export { REGISTRY_READ_SCOPE, REGISTRY_WRITE_SCOPE, scopesForRegistryRole, registryPrincipalHasScope, type RegistryAccessScope, type RegistryPrincipal, type RegistryUser, type RegistryUserRole, type RegistryUserStatus, } from './types';
|
|
11
|
+
export { validateRegistryDisplayName, validateRegistryEmail, validateRegistryImageUrl } from './validation';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setRegistryUserStatus = exports.updateRegistryUserRole = exports.updateRegistryUserProfile = exports.authenticateRegistryCredentials = exports.listRegistryUsers = exports.getRegistryUserById = exports.hashRateLimitIdentifier = exports.purgeExpiredAuthRateLimits = exports.clearAuthRateLimit = exports.consumeAuthRateLimit = exports.MAXIMUM_PASSWORD_LENGTH = exports.MINIMUM_PASSWORD_LENGTH = exports.validatePassword = exports.verifyPassword = exports.hashPassword = exports.REGISTRY_INSTALLATION_ID = exports.REGISTRY_AUTH_SCHEMA_VERSION = exports.installRegistry = exports.getRegistryInstallationState = exports.getRegistryInstallation = exports.DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS = exports.DEVICE_AUTHORIZATION_LIFETIME_SECONDS = exports.normalizeRegistryUserCode = exports.generateRegistryUserCode = exports.purgeExpiredRegistryDeviceAuthorizations = exports.exchangeRegistryDeviceAuthorization = exports.denyRegistryDeviceAuthorization = exports.approveRegistryDeviceAuthorization = exports.createRegistryDeviceAuthorization = exports.buildGravatarUrl = exports.secretHashMatches = exports.normalizeEmail = exports.hashSecret = exports.createOpaqueSecret = exports.PASSWORD_RESET_LIFETIME_MS = exports.INVITATION_LIFETIME_MS = exports.resetPassword = exports.createPasswordReset = exports.acceptUserInvitation = exports.resendUserInvitation = exports.createUserInvitation = exports.registryRoleAllowsScopes = exports.normalizeRegistryScopes = exports.parseRegistryAccessToken = exports.formatRegistryAccessToken = exports.authenticateRegistryAccessToken = exports.revokeAllRegistryAccessTokens = exports.revokeRegistryAccessToken = exports.listRegistryAccessTokens = exports.createRegistryAccessToken = void 0;
|
|
4
|
+
exports.validateRegistryImageUrl = exports.validateRegistryEmail = exports.validateRegistryDisplayName = exports.registryPrincipalHasScope = exports.scopesForRegistryRole = exports.REGISTRY_WRITE_SCOPE = exports.REGISTRY_READ_SCOPE = void 0;
|
|
5
|
+
var access_tokens_1 = require("./access-tokens");
|
|
6
|
+
Object.defineProperty(exports, "createRegistryAccessToken", { enumerable: true, get: function () { return access_tokens_1.createRegistryAccessToken; } });
|
|
7
|
+
Object.defineProperty(exports, "listRegistryAccessTokens", { enumerable: true, get: function () { return access_tokens_1.listRegistryAccessTokens; } });
|
|
8
|
+
Object.defineProperty(exports, "revokeRegistryAccessToken", { enumerable: true, get: function () { return access_tokens_1.revokeRegistryAccessToken; } });
|
|
9
|
+
Object.defineProperty(exports, "revokeAllRegistryAccessTokens", { enumerable: true, get: function () { return access_tokens_1.revokeAllRegistryAccessTokens; } });
|
|
10
|
+
Object.defineProperty(exports, "authenticateRegistryAccessToken", { enumerable: true, get: function () { return access_tokens_1.authenticateRegistryAccessToken; } });
|
|
11
|
+
Object.defineProperty(exports, "formatRegistryAccessToken", { enumerable: true, get: function () { return access_tokens_1.formatRegistryAccessToken; } });
|
|
12
|
+
Object.defineProperty(exports, "parseRegistryAccessToken", { enumerable: true, get: function () { return access_tokens_1.parseRegistryAccessToken; } });
|
|
13
|
+
Object.defineProperty(exports, "normalizeRegistryScopes", { enumerable: true, get: function () { return access_tokens_1.normalizeRegistryScopes; } });
|
|
14
|
+
Object.defineProperty(exports, "registryRoleAllowsScopes", { enumerable: true, get: function () { return access_tokens_1.registryRoleAllowsScopes; } });
|
|
15
|
+
var action_tokens_1 = require("./action-tokens");
|
|
16
|
+
Object.defineProperty(exports, "createUserInvitation", { enumerable: true, get: function () { return action_tokens_1.createUserInvitation; } });
|
|
17
|
+
Object.defineProperty(exports, "resendUserInvitation", { enumerable: true, get: function () { return action_tokens_1.resendUserInvitation; } });
|
|
18
|
+
Object.defineProperty(exports, "acceptUserInvitation", { enumerable: true, get: function () { return action_tokens_1.acceptUserInvitation; } });
|
|
19
|
+
Object.defineProperty(exports, "createPasswordReset", { enumerable: true, get: function () { return action_tokens_1.createPasswordReset; } });
|
|
20
|
+
Object.defineProperty(exports, "resetPassword", { enumerable: true, get: function () { return action_tokens_1.resetPassword; } });
|
|
21
|
+
Object.defineProperty(exports, "INVITATION_LIFETIME_MS", { enumerable: true, get: function () { return action_tokens_1.INVITATION_LIFETIME_MS; } });
|
|
22
|
+
Object.defineProperty(exports, "PASSWORD_RESET_LIFETIME_MS", { enumerable: true, get: function () { return action_tokens_1.PASSWORD_RESET_LIFETIME_MS; } });
|
|
23
|
+
var crypto_1 = require("./crypto");
|
|
24
|
+
Object.defineProperty(exports, "createOpaqueSecret", { enumerable: true, get: function () { return crypto_1.createOpaqueSecret; } });
|
|
25
|
+
Object.defineProperty(exports, "hashSecret", { enumerable: true, get: function () { return crypto_1.hashSecret; } });
|
|
26
|
+
Object.defineProperty(exports, "normalizeEmail", { enumerable: true, get: function () { return crypto_1.normalizeEmail; } });
|
|
27
|
+
Object.defineProperty(exports, "secretHashMatches", { enumerable: true, get: function () { return crypto_1.secretHashMatches; } });
|
|
28
|
+
var gravatar_1 = require("./gravatar");
|
|
29
|
+
Object.defineProperty(exports, "buildGravatarUrl", { enumerable: true, get: function () { return gravatar_1.buildGravatarUrl; } });
|
|
30
|
+
var device_authorizations_1 = require("./device-authorizations");
|
|
31
|
+
Object.defineProperty(exports, "createRegistryDeviceAuthorization", { enumerable: true, get: function () { return device_authorizations_1.createRegistryDeviceAuthorization; } });
|
|
32
|
+
Object.defineProperty(exports, "approveRegistryDeviceAuthorization", { enumerable: true, get: function () { return device_authorizations_1.approveRegistryDeviceAuthorization; } });
|
|
33
|
+
Object.defineProperty(exports, "denyRegistryDeviceAuthorization", { enumerable: true, get: function () { return device_authorizations_1.denyRegistryDeviceAuthorization; } });
|
|
34
|
+
Object.defineProperty(exports, "exchangeRegistryDeviceAuthorization", { enumerable: true, get: function () { return device_authorizations_1.exchangeRegistryDeviceAuthorization; } });
|
|
35
|
+
Object.defineProperty(exports, "purgeExpiredRegistryDeviceAuthorizations", { enumerable: true, get: function () { return device_authorizations_1.purgeExpiredRegistryDeviceAuthorizations; } });
|
|
36
|
+
Object.defineProperty(exports, "generateRegistryUserCode", { enumerable: true, get: function () { return device_authorizations_1.generateRegistryUserCode; } });
|
|
37
|
+
Object.defineProperty(exports, "normalizeRegistryUserCode", { enumerable: true, get: function () { return device_authorizations_1.normalizeRegistryUserCode; } });
|
|
38
|
+
Object.defineProperty(exports, "DEVICE_AUTHORIZATION_LIFETIME_SECONDS", { enumerable: true, get: function () { return device_authorizations_1.DEVICE_AUTHORIZATION_LIFETIME_SECONDS; } });
|
|
39
|
+
Object.defineProperty(exports, "DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS", { enumerable: true, get: function () { return device_authorizations_1.DEVICE_AUTHORIZATION_POLL_INTERVAL_SECONDS; } });
|
|
40
|
+
var installation_1 = require("./installation");
|
|
41
|
+
Object.defineProperty(exports, "getRegistryInstallation", { enumerable: true, get: function () { return installation_1.getRegistryInstallation; } });
|
|
42
|
+
Object.defineProperty(exports, "getRegistryInstallationState", { enumerable: true, get: function () { return installation_1.getRegistryInstallationState; } });
|
|
43
|
+
Object.defineProperty(exports, "installRegistry", { enumerable: true, get: function () { return installation_1.installRegistry; } });
|
|
44
|
+
Object.defineProperty(exports, "REGISTRY_AUTH_SCHEMA_VERSION", { enumerable: true, get: function () { return installation_1.REGISTRY_AUTH_SCHEMA_VERSION; } });
|
|
45
|
+
Object.defineProperty(exports, "REGISTRY_INSTALLATION_ID", { enumerable: true, get: function () { return installation_1.REGISTRY_INSTALLATION_ID; } });
|
|
46
|
+
var password_1 = require("./password");
|
|
47
|
+
Object.defineProperty(exports, "hashPassword", { enumerable: true, get: function () { return password_1.hashPassword; } });
|
|
48
|
+
Object.defineProperty(exports, "verifyPassword", { enumerable: true, get: function () { return password_1.verifyPassword; } });
|
|
49
|
+
Object.defineProperty(exports, "validatePassword", { enumerable: true, get: function () { return password_1.validatePassword; } });
|
|
50
|
+
Object.defineProperty(exports, "MINIMUM_PASSWORD_LENGTH", { enumerable: true, get: function () { return password_1.MINIMUM_PASSWORD_LENGTH; } });
|
|
51
|
+
Object.defineProperty(exports, "MAXIMUM_PASSWORD_LENGTH", { enumerable: true, get: function () { return password_1.MAXIMUM_PASSWORD_LENGTH; } });
|
|
52
|
+
var rate_limits_1 = require("./rate-limits");
|
|
53
|
+
Object.defineProperty(exports, "consumeAuthRateLimit", { enumerable: true, get: function () { return rate_limits_1.consumeAuthRateLimit; } });
|
|
54
|
+
Object.defineProperty(exports, "clearAuthRateLimit", { enumerable: true, get: function () { return rate_limits_1.clearAuthRateLimit; } });
|
|
55
|
+
Object.defineProperty(exports, "purgeExpiredAuthRateLimits", { enumerable: true, get: function () { return rate_limits_1.purgeExpiredAuthRateLimits; } });
|
|
56
|
+
Object.defineProperty(exports, "hashRateLimitIdentifier", { enumerable: true, get: function () { return rate_limits_1.hashRateLimitIdentifier; } });
|
|
57
|
+
var users_1 = require("./users");
|
|
58
|
+
Object.defineProperty(exports, "getRegistryUserById", { enumerable: true, get: function () { return users_1.getRegistryUserById; } });
|
|
59
|
+
Object.defineProperty(exports, "listRegistryUsers", { enumerable: true, get: function () { return users_1.listRegistryUsers; } });
|
|
60
|
+
Object.defineProperty(exports, "authenticateRegistryCredentials", { enumerable: true, get: function () { return users_1.authenticateRegistryCredentials; } });
|
|
61
|
+
Object.defineProperty(exports, "updateRegistryUserProfile", { enumerable: true, get: function () { return users_1.updateRegistryUserProfile; } });
|
|
62
|
+
Object.defineProperty(exports, "updateRegistryUserRole", { enumerable: true, get: function () { return users_1.updateRegistryUserRole; } });
|
|
63
|
+
Object.defineProperty(exports, "setRegistryUserStatus", { enumerable: true, get: function () { return users_1.setRegistryUserStatus; } });
|
|
64
|
+
var types_1 = require("./types");
|
|
65
|
+
Object.defineProperty(exports, "REGISTRY_READ_SCOPE", { enumerable: true, get: function () { return types_1.REGISTRY_READ_SCOPE; } });
|
|
66
|
+
Object.defineProperty(exports, "REGISTRY_WRITE_SCOPE", { enumerable: true, get: function () { return types_1.REGISTRY_WRITE_SCOPE; } });
|
|
67
|
+
Object.defineProperty(exports, "scopesForRegistryRole", { enumerable: true, get: function () { return types_1.scopesForRegistryRole; } });
|
|
68
|
+
Object.defineProperty(exports, "registryPrincipalHasScope", { enumerable: true, get: function () { return types_1.registryPrincipalHasScope; } });
|
|
69
|
+
var validation_1 = require("./validation");
|
|
70
|
+
Object.defineProperty(exports, "validateRegistryDisplayName", { enumerable: true, get: function () { return validation_1.validateRegistryDisplayName; } });
|
|
71
|
+
Object.defineProperty(exports, "validateRegistryEmail", { enumerable: true, get: function () { return validation_1.validateRegistryEmail; } });
|
|
72
|
+
Object.defineProperty(exports, "validateRegistryImageUrl", { enumerable: true, get: function () { return validation_1.validateRegistryImageUrl; } });
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { RegistryDatabase } from '../db/client';
|
|
2
|
+
import { registryInstallations } from '../db/schema';
|
|
3
|
+
import type { RegistryUser } from './types';
|
|
4
|
+
/** Migration number that introduced the registry authentication schema. */
|
|
5
|
+
export declare const REGISTRY_AUTH_SCHEMA_VERSION = 5;
|
|
6
|
+
export declare const REGISTRY_INSTALLATION_ID = "default";
|
|
7
|
+
export type RegistryInstallation = typeof registryInstallations.$inferSelect;
|
|
8
|
+
export type RegistryInstallationState = {
|
|
9
|
+
status: 'ready';
|
|
10
|
+
installation: null;
|
|
11
|
+
userCount: 0;
|
|
12
|
+
} | {
|
|
13
|
+
status: 'installed';
|
|
14
|
+
installation: RegistryInstallation;
|
|
15
|
+
userCount: number;
|
|
16
|
+
} | {
|
|
17
|
+
status: 'inconsistent';
|
|
18
|
+
installation: null;
|
|
19
|
+
userCount: number;
|
|
20
|
+
};
|
|
21
|
+
export interface RegistryInstallInput {
|
|
22
|
+
email: string;
|
|
23
|
+
name: string;
|
|
24
|
+
password: string;
|
|
25
|
+
now?: Date;
|
|
26
|
+
}
|
|
27
|
+
export type RegistryInstallResult = {
|
|
28
|
+
ok: true;
|
|
29
|
+
user: RegistryUser;
|
|
30
|
+
installation: RegistryInstallation;
|
|
31
|
+
} | {
|
|
32
|
+
ok: false;
|
|
33
|
+
reason: 'invalid_email' | 'invalid_name' | 'invalid_password' | 'already_installed' | 'users_exist';
|
|
34
|
+
error?: string;
|
|
35
|
+
};
|
|
36
|
+
export declare const getRegistryInstallation: (db: RegistryDatabase) => Promise<RegistryInstallation | null>;
|
|
37
|
+
export declare const getRegistryInstallationState: (db: RegistryDatabase) => Promise<RegistryInstallationState>;
|
|
38
|
+
/**
|
|
39
|
+
* Complete first-visitor installation under a transaction-scoped advisory lock. Password hashing is
|
|
40
|
+
* intentionally performed before taking the lock; all state checks are repeated inside it.
|
|
41
|
+
*/
|
|
42
|
+
export declare const installRegistry: (db: RegistryDatabase, input: RegistryInstallInput) => Promise<RegistryInstallResult>;
|