@isahaq/kosh-server 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +21 -2
- package/dist/api/client.d.ts +18 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +380 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/idempotency.d.ts +23 -0
- package/dist/api/idempotency.d.ts.map +1 -0
- package/dist/api/idempotency.js +52 -0
- package/dist/api/idempotency.js.map +1 -0
- package/dist/api/metadata.d.ts +65 -0
- package/dist/api/metadata.d.ts.map +1 -0
- package/dist/api/metadata.js +88 -0
- package/dist/api/metadata.js.map +1 -0
- package/dist/api/navigation.d.ts +46 -0
- package/dist/api/navigation.d.ts.map +1 -0
- package/dist/api/navigation.js +131 -0
- package/dist/api/navigation.js.map +1 -0
- package/dist/api/openapi.d.ts +22 -0
- package/dist/api/openapi.d.ts.map +1 -0
- package/dist/api/openapi.js +349 -0
- package/dist/api/openapi.js.map +1 -0
- package/dist/api/rest.d.ts +28 -0
- package/dist/api/rest.d.ts.map +1 -0
- package/dist/api/rest.js +435 -0
- package/dist/api/rest.js.map +1 -0
- package/dist/api/views.d.ts +67 -0
- package/dist/api/views.d.ts.map +1 -0
- package/dist/api/views.js +307 -0
- package/dist/api/views.js.map +1 -0
- package/dist/audit.d.ts +21 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +26 -0
- package/dist/audit.js.map +1 -0
- package/dist/auth/admins.d.ts +26 -0
- package/dist/auth/admins.d.ts.map +1 -0
- package/dist/auth/admins.js +70 -0
- package/dist/auth/admins.js.map +1 -0
- package/dist/auth/base.d.ts +12 -0
- package/dist/auth/base.d.ts.map +1 -0
- package/dist/auth/base.js +192 -0
- package/dist/auth/base.js.map +1 -0
- package/dist/auth/crypto.d.ts +20 -0
- package/dist/auth/crypto.d.ts.map +1 -0
- package/dist/auth/crypto.js +100 -0
- package/dist/auth/crypto.js.map +1 -0
- package/dist/auth/install.d.ts +24 -0
- package/dist/auth/install.d.ts.map +1 -0
- package/dist/auth/install.js +18 -0
- package/dist/auth/install.js.map +1 -0
- package/dist/auth/routes.d.ts +20 -0
- package/dist/auth/routes.d.ts.map +1 -0
- package/dist/auth/routes.js +56 -0
- package/dist/auth/routes.js.map +1 -0
- package/dist/auth/service.d.ts +108 -0
- package/dist/auth/service.d.ts.map +1 -0
- package/dist/auth/service.js +477 -0
- package/dist/auth/service.js.map +1 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +78 -0
- package/dist/errors.js.map +1 -0
- package/dist/identity.d.ts +49 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +58 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/orm.d.ts +14 -0
- package/dist/orm.d.ts.map +1 -0
- package/dist/orm.js +12 -0
- package/dist/orm.js.map +1 -0
- package/dist/registry.d.ts +19 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +26 -0
- package/dist/registry.js.map +1 -0
- package/dist/server.d.ts +93 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +223 -0
- package/dist/server.js.map +1 -0
- package/dist/telemetry.d.ts +23 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +38 -0
- package/dist/telemetry.js.map +1 -0
- package/package.json +63 -3
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The framework's `base` module (CLAUDE.md, Phase 1: users and companies): currencies, companies, users, and the
|
|
3
|
+
* private authentication models (credentials, sessions, per-company group assignments). Every Kosh application
|
|
4
|
+
* depends on it. Administrators (`base.admin`) need no grants (RFC 0003 §5.2).
|
|
5
|
+
*/
|
|
6
|
+
import { defineModule as defineKoshModule } from '@isahaq/kosh-modules-sdk';
|
|
7
|
+
import { orm } from '../orm.js';
|
|
8
|
+
import { LAST_ADMINISTRATOR, lastAdministratorRemoved } from './admins.js';
|
|
9
|
+
const { defineGroup, defineModel, defineModule, f } = orm;
|
|
10
|
+
const ResCurrency = defineModel('res.currency', {
|
|
11
|
+
access: { 'base.user': ['read'] },
|
|
12
|
+
fields: {
|
|
13
|
+
name: f.char({ required: true, size: 3, index: 'unique' }),
|
|
14
|
+
rounding: f.decimal({ scale: 6, required: true, default: '0.01' }),
|
|
15
|
+
decimalPlaces: f.integer({ default: 2 }),
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
const ResCompany = defineModel('res.company', {
|
|
19
|
+
access: { 'base.user': ['read'] },
|
|
20
|
+
fields: {
|
|
21
|
+
name: f.char({ required: true }),
|
|
22
|
+
currency: f.many2one('res.currency', { required: true }),
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
const readRows = async (env, model, ids, keys) => env.asSuperuser({ reason: 'check administrators' }, async (su) => {
|
|
26
|
+
const set = su.model(model).browse(ids);
|
|
27
|
+
return (await set.load(keys)).read(keys);
|
|
28
|
+
});
|
|
29
|
+
const ResUsers = defineModel('res.users', {
|
|
30
|
+
archivable: true, // `active`: deactivated users cannot sign in
|
|
31
|
+
access: { 'base.user': ['read'] },
|
|
32
|
+
constraints: {
|
|
33
|
+
// Archiving the last administrator is refused (users are archived, not deleted: see auth.user_group.user).
|
|
34
|
+
keepAdministrator: {
|
|
35
|
+
depends: ['active'],
|
|
36
|
+
check: (records) => lastAdministratorRemoved(records, async (env, ids) => (await readRows(env, 'res.users', ids, ['active'])).map((r) => ({
|
|
37
|
+
id: r['id'],
|
|
38
|
+
user: r['id'],
|
|
39
|
+
removed: r['active'] === false,
|
|
40
|
+
}))),
|
|
41
|
+
message: LAST_ADMINISTRATOR,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
fields: {
|
|
45
|
+
login: f.char({ required: true, index: 'unique' }),
|
|
46
|
+
name: f.char({ required: true }),
|
|
47
|
+
email: f.char(),
|
|
48
|
+
company: f.many2one('res.company'),
|
|
49
|
+
companies: f.many2many('res.company'),
|
|
50
|
+
lang: f.char({ default: 'en_US' }),
|
|
51
|
+
tz: f.char({ default: 'UTC' }),
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
/** Password hash and TOTP state of a user. Private: never served by the generated API. */
|
|
55
|
+
const AuthCredential = defineModel('auth.credential', {
|
|
56
|
+
fields: {
|
|
57
|
+
user: f.many2one('res.users', { required: true, ondelete: 'cascade', index: 'unique' }),
|
|
58
|
+
passwordHash: f.char(),
|
|
59
|
+
totpSecret: f.char(), // pending or active secret
|
|
60
|
+
totpEnabled: f.boolean({ default: false }),
|
|
61
|
+
recoveryCodes: f.json(), // SHA-256 digests of unused codes
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
/** A signed-in session; the token is stored only as a SHA-256 digest. Private. */
|
|
65
|
+
const AuthSession = defineModel('auth.session', {
|
|
66
|
+
fields: {
|
|
67
|
+
tokenHash: f.char({ required: true, index: 'unique' }),
|
|
68
|
+
csrfHash: f.char({ required: true }),
|
|
69
|
+
user: f.many2one('res.users', { required: true, ondelete: 'cascade' }),
|
|
70
|
+
twoFactorVerified: f.boolean({ default: false }),
|
|
71
|
+
/** A second factor was entered in this session (sign-in or enrolment): administrator groups need it. */
|
|
72
|
+
secondFactor: f.boolean({ default: false }),
|
|
73
|
+
expiresAt: f.datetime({ required: true }),
|
|
74
|
+
lastSeenAt: f.datetime({ required: true }),
|
|
75
|
+
revoked: f.boolean({ default: false }),
|
|
76
|
+
ip: f.char(),
|
|
77
|
+
userAgent: f.char(),
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
/**
|
|
81
|
+
* A group or role a user holds in one company (or every company when `company` is empty). Private. Users holding
|
|
82
|
+
* any cannot be deleted (`restrict`): they are archived, which the "keep one administrator" guard checks.
|
|
83
|
+
*/
|
|
84
|
+
const AuthUserGroup = defineModel('auth.user_group', {
|
|
85
|
+
fields: {
|
|
86
|
+
user: f.many2one('res.users', { required: true, ondelete: 'restrict' }),
|
|
87
|
+
company: f.many2one('res.company', { ondelete: 'cascade' }),
|
|
88
|
+
group: f.char({ required: true }),
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
/**
|
|
92
|
+
* A special permission of one user (Shutro `grants`: a feature permission such as `account.backdate`, or
|
|
93
|
+
* `model:perm`), in one company or all, optionally until a date. Private: managed by the admin API.
|
|
94
|
+
*/
|
|
95
|
+
const AuthUserGrant = defineModel('auth.user_grant', {
|
|
96
|
+
fields: {
|
|
97
|
+
user: f.many2one('res.users', { required: true, ondelete: 'cascade' }),
|
|
98
|
+
company: f.many2one('res.company', { ondelete: 'cascade' }),
|
|
99
|
+
grant: f.char({ required: true }),
|
|
100
|
+
expiresAt: f.datetime(),
|
|
101
|
+
reason: f.char({ required: true }),
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
/**
|
|
105
|
+
* The security audit log (CLAUDE.md: logins, permission changes, exports, sensitive actions). Append-only: no one
|
|
106
|
+
* may change or delete events (administrators included: the REST API never serves it, and the admin API only reads).
|
|
107
|
+
*/
|
|
108
|
+
const AuditEvent = defineModel('audit.event', {
|
|
109
|
+
access: { 'base.user': ['create'] },
|
|
110
|
+
order: ['id desc'],
|
|
111
|
+
fields: {
|
|
112
|
+
kind: f.char({ required: true, index: true }),
|
|
113
|
+
actor: f.many2one('res.users', { ondelete: 'set null' }),
|
|
114
|
+
company: f.many2one('res.company', { ondelete: 'set null' }),
|
|
115
|
+
targetModel: f.char(),
|
|
116
|
+
targetId: f.integer(),
|
|
117
|
+
summary: f.char({ required: true }),
|
|
118
|
+
details: f.json(),
|
|
119
|
+
ip: f.char(),
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
/**
|
|
123
|
+
* Saved searches and favorites (RFC 0004 §5): a search state for a model and view type, private to its owner or
|
|
124
|
+
* shared with every user who can read the model. Governed by record rules, served by the generated API.
|
|
125
|
+
*/
|
|
126
|
+
const SavedView = defineModel('kosh.saved_view', {
|
|
127
|
+
access: { 'base.user': ['read', 'write', 'create', 'unlink'] },
|
|
128
|
+
rules: {
|
|
129
|
+
visible: {
|
|
130
|
+
groups: ['base.user'],
|
|
131
|
+
perms: ['read'],
|
|
132
|
+
domain: { or: [{ createdBy: { $env: 'uid' } }, { shared: true }] },
|
|
133
|
+
},
|
|
134
|
+
own: {
|
|
135
|
+
groups: ['base.user'],
|
|
136
|
+
perms: ['write', 'unlink'],
|
|
137
|
+
domain: { createdBy: { $env: 'uid' } },
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
order: ['name asc'],
|
|
141
|
+
fields: {
|
|
142
|
+
name: f.char({ required: true }),
|
|
143
|
+
targetModel: f.char({ required: true, index: true }),
|
|
144
|
+
viewType: f.char({ required: true, default: 'list' }),
|
|
145
|
+
/** The `@isahaq/kosh-search` state (facets, filters, group-by, order). */
|
|
146
|
+
state: f.json({ required: true }),
|
|
147
|
+
shared: f.boolean({ default: false }),
|
|
148
|
+
isDefault: f.boolean({ default: false }),
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
/** The Shutro module behind {@link base}. */
|
|
152
|
+
export const baseShutro = defineModule({
|
|
153
|
+
name: 'base',
|
|
154
|
+
version: '0.1.0',
|
|
155
|
+
groups: [
|
|
156
|
+
defineGroup({ code: 'base.user', label: 'Internal user' }),
|
|
157
|
+
defineGroup({
|
|
158
|
+
code: 'base.admin',
|
|
159
|
+
label: 'Administrator',
|
|
160
|
+
implies: ['base.user'],
|
|
161
|
+
administrator: true,
|
|
162
|
+
}),
|
|
163
|
+
],
|
|
164
|
+
models: [
|
|
165
|
+
ResCurrency,
|
|
166
|
+
ResCompany,
|
|
167
|
+
ResUsers,
|
|
168
|
+
AuthCredential,
|
|
169
|
+
AuthSession,
|
|
170
|
+
AuthUserGroup,
|
|
171
|
+
AuthUserGrant,
|
|
172
|
+
AuditEvent,
|
|
173
|
+
SavedView,
|
|
174
|
+
],
|
|
175
|
+
});
|
|
176
|
+
/** The framework's base module: depend on it from every application module. */
|
|
177
|
+
export const base = defineKoshModule({
|
|
178
|
+
shutro: baseShutro,
|
|
179
|
+
kosh: '^0.1.0',
|
|
180
|
+
accessLevels: {
|
|
181
|
+
user: { label: 'Internal user', groups: ['base.user'] },
|
|
182
|
+
admin: { label: 'Administrator', groups: ['base.admin'], implies: ['user'] },
|
|
183
|
+
},
|
|
184
|
+
privateModels: [
|
|
185
|
+
'auth.credential',
|
|
186
|
+
'auth.session',
|
|
187
|
+
'auth.user_group',
|
|
188
|
+
'auth.user_grant',
|
|
189
|
+
'audit.event',
|
|
190
|
+
],
|
|
191
|
+
});
|
|
192
|
+
//# sourceMappingURL=base.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.js","sourceRoot":"","sources":["../../src/auth/base.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** `scrypt$N$r$p$salt$hash` (base64url); the parameters travel with the hash, so they can be raised later. */
|
|
2
|
+
export declare function hashPassword(password: string): Promise<string>;
|
|
3
|
+
/** Constant-time check of a password against a stored hash (false for malformed hashes). */
|
|
4
|
+
export declare function verifyPassword(password: string, stored: string): Promise<boolean>;
|
|
5
|
+
/** A random token (base64url). */
|
|
6
|
+
export declare function randomToken(bytes?: number): string;
|
|
7
|
+
/** SHA-256 of a token (hex), as stored for sessions and recovery codes. */
|
|
8
|
+
export declare function digest(value: string): string;
|
|
9
|
+
/** RFC 4648 base32 (no padding), as authenticator apps expect secrets. */
|
|
10
|
+
export declare function base32Encode(bytes: Buffer): string;
|
|
11
|
+
export declare function base32Decode(text: string): Buffer;
|
|
12
|
+
/** A new TOTP secret (160 bits, base32). */
|
|
13
|
+
export declare function newTotpSecret(): string;
|
|
14
|
+
/** The TOTP code for `secret` at `time` (6 digits, 30 s steps, HMAC-SHA1: what authenticator apps use). */
|
|
15
|
+
export declare function totp(secret: string, time?: number, digits?: number, stepSeconds?: number): string;
|
|
16
|
+
/** True when `code` matches the current step or one step either side (clock drift). */
|
|
17
|
+
export declare function verifyTotp(secret: string, code: string, time?: number): boolean;
|
|
18
|
+
/** The `otpauth://` URI authenticator apps scan. */
|
|
19
|
+
export declare function otpauthUri(secret: string, account: string, issuer?: string): string;
|
|
20
|
+
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/auth/crypto.ts"],"names":[],"mappings":"AAiCA,8GAA8G;AAC9G,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAIpE;AAED,4FAA4F;AAC5F,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAavF;AAED,kCAAkC;AAClC,wBAAgB,WAAW,CAAC,KAAK,SAAK,GAAG,MAAM,CAE9C;AAED,2EAA2E;AAC3E,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5C;AAMD,0EAA0E;AAC1E,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAclD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAgBjD;AAED,4CAA4C;AAC5C,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,2GAA2G;AAC3G,wBAAgB,IAAI,CAClB,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,MAAmB,EACzB,MAAM,SAAI,EACV,WAAW,SAAK,GACf,MAAM,CAQR;AAED,uFAAuF;AACvF,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAmB,GAAG,OAAO,CAO3F;AAED,oDAAoD;AACpD,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,GAAG,MAAM,CAEnF"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credentials without dependencies (node:crypto): scrypt password hashes, RFC 6238 TOTP for two-factor
|
|
3
|
+
* authentication, random tokens, and SHA-256 digests for values stored only as hashes (sessions, recovery codes).
|
|
4
|
+
*/
|
|
5
|
+
import { createHash, createHmac, randomBytes, scrypt as scryptCallback, timingSafeEqual, } from 'node:crypto';
|
|
6
|
+
const SCRYPT = { N: 2 ** 15, r: 8, p: 1, keylen: 64, maxmem: 64 * 1024 * 1024 };
|
|
7
|
+
function scrypt(password, salt, n, r, p, keylen) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
scryptCallback(password.normalize('NFKC'), salt, keylen, { N: n, r, p, maxmem: SCRYPT.maxmem }, (error, key) => (error === null ? resolve(key) : reject(error)));
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
/** `scrypt$N$r$p$salt$hash` (base64url); the parameters travel with the hash, so they can be raised later. */
|
|
13
|
+
export async function hashPassword(password) {
|
|
14
|
+
const salt = randomBytes(16);
|
|
15
|
+
const key = await scrypt(password, salt, SCRYPT.N, SCRYPT.r, SCRYPT.p, SCRYPT.keylen);
|
|
16
|
+
return `scrypt$${SCRYPT.N}$${SCRYPT.r}$${SCRYPT.p}$${salt.toString('base64url')}$${key.toString('base64url')}`;
|
|
17
|
+
}
|
|
18
|
+
/** Constant-time check of a password against a stored hash (false for malformed hashes). */
|
|
19
|
+
export async function verifyPassword(password, stored) {
|
|
20
|
+
const [scheme, n, r, p, salt, hash] = stored.split('$');
|
|
21
|
+
if (scheme !== 'scrypt' || salt === undefined || hash === undefined)
|
|
22
|
+
return false;
|
|
23
|
+
const expected = Buffer.from(hash, 'base64url');
|
|
24
|
+
const key = await scrypt(password, Buffer.from(salt, 'base64url'), Number(n), Number(r), Number(p), expected.length);
|
|
25
|
+
return key.length === expected.length && timingSafeEqual(key, expected);
|
|
26
|
+
}
|
|
27
|
+
/** A random token (base64url). */
|
|
28
|
+
export function randomToken(bytes = 32) {
|
|
29
|
+
return randomBytes(bytes).toString('base64url');
|
|
30
|
+
}
|
|
31
|
+
/** SHA-256 of a token (hex), as stored for sessions and recovery codes. */
|
|
32
|
+
export function digest(value) {
|
|
33
|
+
return createHash('sha256').update(value).digest('hex');
|
|
34
|
+
}
|
|
35
|
+
// ------------------------------------------------------------------------------------------------- TOTP (RFC 6238)
|
|
36
|
+
const BASE32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
37
|
+
/** RFC 4648 base32 (no padding), as authenticator apps expect secrets. */
|
|
38
|
+
export function base32Encode(bytes) {
|
|
39
|
+
let bits = 0;
|
|
40
|
+
let value = 0;
|
|
41
|
+
let out = '';
|
|
42
|
+
for (const byte of bytes) {
|
|
43
|
+
value = (value << 8) | byte;
|
|
44
|
+
bits += 8;
|
|
45
|
+
while (bits >= 5) {
|
|
46
|
+
out += BASE32[(value >>> (bits - 5)) & 31];
|
|
47
|
+
bits -= 5;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (bits > 0)
|
|
51
|
+
out += BASE32[(value << (5 - bits)) & 31];
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
export function base32Decode(text) {
|
|
55
|
+
const clean = text.replace(/=+$/, '').toUpperCase();
|
|
56
|
+
let bits = 0;
|
|
57
|
+
let value = 0;
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const char of clean) {
|
|
60
|
+
const index = BASE32.indexOf(char);
|
|
61
|
+
if (index < 0)
|
|
62
|
+
throw new RangeError('Not a base32 string.');
|
|
63
|
+
value = (value << 5) | index;
|
|
64
|
+
bits += 5;
|
|
65
|
+
if (bits >= 8) {
|
|
66
|
+
out.push((value >>> (bits - 8)) & 255);
|
|
67
|
+
bits -= 8;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return Buffer.from(out);
|
|
71
|
+
}
|
|
72
|
+
/** A new TOTP secret (160 bits, base32). */
|
|
73
|
+
export function newTotpSecret() {
|
|
74
|
+
return base32Encode(randomBytes(20));
|
|
75
|
+
}
|
|
76
|
+
/** The TOTP code for `secret` at `time` (6 digits, 30 s steps, HMAC-SHA1: what authenticator apps use). */
|
|
77
|
+
export function totp(secret, time = Date.now(), digits = 6, stepSeconds = 30) {
|
|
78
|
+
const counter = Math.floor(time / 1000 / stepSeconds);
|
|
79
|
+
const message = Buffer.alloc(8);
|
|
80
|
+
message.writeBigUInt64BE(BigInt(counter));
|
|
81
|
+
const mac = createHmac('sha1', base32Decode(secret)).update(message).digest();
|
|
82
|
+
const offset = mac[mac.length - 1] & 15;
|
|
83
|
+
const binary = (mac.readUInt32BE(offset) & 0x7fffffff) % 10 ** digits;
|
|
84
|
+
return String(binary).padStart(digits, '0');
|
|
85
|
+
}
|
|
86
|
+
/** True when `code` matches the current step or one step either side (clock drift). */
|
|
87
|
+
export function verifyTotp(secret, code, time = Date.now()) {
|
|
88
|
+
if (!/^\d{6}$/.test(code))
|
|
89
|
+
return false;
|
|
90
|
+
return [-1, 0, 1].some((drift) => {
|
|
91
|
+
const expected = Buffer.from(totp(secret, time + drift * 30_000));
|
|
92
|
+
const given = Buffer.from(code);
|
|
93
|
+
return expected.length === given.length && timingSafeEqual(expected, given);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/** The `otpauth://` URI authenticator apps scan. */
|
|
97
|
+
export function otpauthUri(secret, account, issuer = 'Kosh') {
|
|
98
|
+
return `otpauth://totp/${encodeURIComponent(`${issuer}:${account}`)}?secret=${secret}&issuer=${encodeURIComponent(issuer)}&algorithm=SHA1&digits=6&period=30`;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/auth/crypto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First installation of `base`: a currency, the first company and the first administrator, in one audited system
|
|
3
|
+
* scope. Run once, after the schema exists.
|
|
4
|
+
*/
|
|
5
|
+
import type { Shutro } from '../orm.js';
|
|
6
|
+
import type { AuthService } from './service.js';
|
|
7
|
+
/** Options of {@link installBase}. */
|
|
8
|
+
export interface InstallBaseOptions {
|
|
9
|
+
readonly company: {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly currency: string;
|
|
12
|
+
};
|
|
13
|
+
readonly admin: {
|
|
14
|
+
readonly login: string;
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly password: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Creates the first company and administrator; returns their ids. */
|
|
20
|
+
export declare function installBase(shutro: Shutro, auth: AuthService, options: InstallBaseOptions): Promise<{
|
|
21
|
+
company: number;
|
|
22
|
+
admin: number;
|
|
23
|
+
}>;
|
|
24
|
+
//# sourceMappingURL=install.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/auth/install.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAO,MAAM,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,sCAAsC;AACtC,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACvE,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9F;AAED,sEAAsE;AACtE,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAoB7C"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Creates the first company and administrator; returns their ids. */
|
|
2
|
+
export async function installBase(shutro, auth, options) {
|
|
3
|
+
const company = await shutro.bootstrap({ reason: 'install base' }, async (env) => {
|
|
4
|
+
const model = (name) => env.model(name);
|
|
5
|
+
const [currency] = (await model('res.currency').create([{ name: options.company.currency }]))
|
|
6
|
+
.ids;
|
|
7
|
+
const [created] = (await model('res.company').create([{ name: options.company.name, currency }])).ids;
|
|
8
|
+
return created;
|
|
9
|
+
});
|
|
10
|
+
const admin = await auth.createUser({
|
|
11
|
+
...options.admin,
|
|
12
|
+
company,
|
|
13
|
+
companies: [company],
|
|
14
|
+
groups: ['base.admin'],
|
|
15
|
+
});
|
|
16
|
+
return { company, admin };
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/auth/install.ts"],"names":[],"mappings":"AAaA,sEAAsE;AACtE,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAc,EACd,IAAiB,EACjB,OAA2B;IAE3B,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,EAAE,KAAK,EAAE,GAAQ,EAAE,EAAE;QACpF,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,EAAE,CAC7B,GAAG,CAAC,KAAK,CAAC,IAAa,CAEtB,CAAC;QACJ,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;aAC1F,GAAG,CAAC;QACP,MAAM,CAAC,OAAO,CAAC,GAAG,CAChB,MAAM,KAAK,CAAC,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAC9E,CAAC,GAAG,CAAC;QACN,OAAO,OAAiB,CAAC;IAC3B,CAAC,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC;QAClC,GAAG,OAAO,CAAC,KAAK;QAChB,OAAO;QACP,SAAS,EAAE,CAAC,OAAO,CAAC;QACpB,MAAM,EAAE,CAAC,YAAY,CAAC;KACvB,CAAC,CAAC;IACH,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAC5B,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication routes (`/api/v1/auth/*`). Sign-in is public but limited per IP and login (brute force);
|
|
3
|
+
* everything else needs a session.
|
|
4
|
+
*
|
|
5
|
+
* - `POST /api/v1/auth/login` `{ login, password }` → `{ token, csrfToken, twoFactorRequired }` + cookie
|
|
6
|
+
* - `POST /api/v1/auth/logout` revokes the session, clears the cookie
|
|
7
|
+
* - `GET /api/v1/auth/me` the signed-in user
|
|
8
|
+
* - `POST /api/v1/auth/2fa/setup` `{ secret, otpauthUri }` (pending until enabled)
|
|
9
|
+
* - `POST /api/v1/auth/2fa/enable` `{ code }` → `{ recoveryCodes }`
|
|
10
|
+
* - `POST /api/v1/auth/2fa/verify` `{ code }` (TOTP or recovery code) completes a pending sign-in
|
|
11
|
+
* - `POST /api/v1/auth/2fa/disable` `{ password, code }`
|
|
12
|
+
* - `POST /api/v1/auth/password` `{ current, next }`; other sessions are revoked
|
|
13
|
+
*/
|
|
14
|
+
import type { FastifyInstance } from 'fastify';
|
|
15
|
+
import type { AuthService } from './service.js';
|
|
16
|
+
/** Registers the routes; `loginLimit` attempts per 15 minutes per IP and login (default 10). */
|
|
17
|
+
export declare function registerAuthRoutes(app: FastifyInstance, auth: AuthService, options?: {
|
|
18
|
+
readonly loginLimit?: number;
|
|
19
|
+
}): void;
|
|
20
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/auth/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,SAAS,CAAC;AAE/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAUhD,gGAAgG;AAChG,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,WAAW,EACjB,OAAO,GAAE;IAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GAC7C,IAAI,CAyEN"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { HttpProblem } from '../errors.js';
|
|
2
|
+
function text(request, key) {
|
|
3
|
+
const body = request.body;
|
|
4
|
+
const value = typeof body === 'object' && body !== null ? body[key] : undefined;
|
|
5
|
+
if (typeof value !== 'string' || value === '' || value.length > 1024)
|
|
6
|
+
throw new HttpProblem(400, 'invalid-body', 'Invalid body', `'${key}' is required (a string).`);
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
/** Registers the routes; `loginLimit` attempts per 15 minutes per IP and login (default 10). */
|
|
10
|
+
export function registerAuthRoutes(app, auth, options = {}) {
|
|
11
|
+
const limited = options.loginLimit !== 0;
|
|
12
|
+
app.post('/api/v1/auth/login', {
|
|
13
|
+
config: {
|
|
14
|
+
auth: false,
|
|
15
|
+
...(limited
|
|
16
|
+
? {
|
|
17
|
+
rateLimit: {
|
|
18
|
+
hook: 'preHandler', // after body parsing: the key includes the login
|
|
19
|
+
max: options.loginLimit ?? 10,
|
|
20
|
+
timeWindow: '15 minutes',
|
|
21
|
+
keyGenerator: (request) => {
|
|
22
|
+
const login = request.body?.login;
|
|
23
|
+
return `login:${request.ip}:${typeof login === 'string' ? login.toLowerCase() : ''}`;
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
: {}),
|
|
28
|
+
},
|
|
29
|
+
}, async (request, reply) => {
|
|
30
|
+
const result = await auth.login(request, text(request, 'login'), text(request, 'password'));
|
|
31
|
+
reply.header('set-cookie', auth.cookie(result.token));
|
|
32
|
+
return result;
|
|
33
|
+
});
|
|
34
|
+
app.post('/api/v1/auth/logout', { config: { auth: false } }, async (request, reply) => {
|
|
35
|
+
const session = await auth.requireSession(request, { allowPending: true });
|
|
36
|
+
await auth.logout(session);
|
|
37
|
+
reply.header('set-cookie', auth.cookie(null));
|
|
38
|
+
return { ok: true };
|
|
39
|
+
});
|
|
40
|
+
app.get('/api/v1/auth/me', { config: { auth: false } }, async (request) => auth.me(await auth.requireSession(request)));
|
|
41
|
+
app.post('/api/v1/auth/2fa/setup', { config: { auth: false } }, async (request) => auth.setupTwoFactor(await auth.requireSession(request)));
|
|
42
|
+
app.post('/api/v1/auth/2fa/enable', { config: { auth: false } }, async (request) => auth.enableTwoFactor(await auth.requireSession(request), text(request, 'code')));
|
|
43
|
+
app.post('/api/v1/auth/2fa/verify', { config: { auth: false } }, async (request) => {
|
|
44
|
+
await auth.verifyTwoFactor(await auth.requireSession(request, { allowPending: true }), text(request, 'code'));
|
|
45
|
+
return { ok: true };
|
|
46
|
+
});
|
|
47
|
+
app.post('/api/v1/auth/2fa/disable', { config: { auth: false } }, async (request) => {
|
|
48
|
+
await auth.disableTwoFactor(await auth.requireSession(request), text(request, 'password'), text(request, 'code'));
|
|
49
|
+
return { ok: true };
|
|
50
|
+
});
|
|
51
|
+
app.post('/api/v1/auth/password', { config: { auth: false } }, async (request) => {
|
|
52
|
+
await auth.changePassword(await auth.requireSession(request), text(request, 'current'), text(request, 'next'));
|
|
53
|
+
return { ok: true };
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/auth/routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kosh-native authentication on Shutro (CLAUDE.md, decided 2026-10-18): sessions, TOTP two-factor authentication,
|
|
3
|
+
* recovery codes, passwords. All reads and writes go through Shutro in audited system scopes (`shutro.bootstrap`), on
|
|
4
|
+
* the private models of the `base` module.
|
|
5
|
+
*
|
|
6
|
+
* Sessions: a random token (cookie `kosh_session`, httpOnly, SameSite=Lax; or `Authorization: Bearer`) stored as a
|
|
7
|
+
* SHA-256 digest, with an idle timeout and an absolute lifetime. Cookie sessions need `X-CSRF-Token` on unsafe
|
|
8
|
+
* methods. A session of a user with 2FA enabled is limited to `/auth/2fa/verify` and `/auth/logout` until verified.
|
|
9
|
+
*/
|
|
10
|
+
import type { FastifyRequest } from 'fastify';
|
|
11
|
+
import type { RequestIdentity } from '../identity.js';
|
|
12
|
+
import type { Env, Shutro, VerifiedIdentity } from '../orm.js';
|
|
13
|
+
/** Options of the session layer. */
|
|
14
|
+
export interface AuthOptions {
|
|
15
|
+
/** `Secure` cookies (default true; turn off only for plain-HTTP development). */
|
|
16
|
+
readonly secureCookies?: boolean;
|
|
17
|
+
/** Idle timeout (default 12 h). */
|
|
18
|
+
readonly idleMs?: number;
|
|
19
|
+
/** Absolute session lifetime (default 7 days). */
|
|
20
|
+
readonly lifetimeMs?: number;
|
|
21
|
+
/** How long a session lookup is cached (default 5 s): bounds how long a revoked session keeps working elsewhere. */
|
|
22
|
+
readonly cacheMs?: number;
|
|
23
|
+
}
|
|
24
|
+
/** A resolved session. */
|
|
25
|
+
export interface SessionInfo {
|
|
26
|
+
readonly id: number;
|
|
27
|
+
readonly user: number;
|
|
28
|
+
readonly twoFactorRequired: boolean;
|
|
29
|
+
/** A second factor was entered in this session: administrator groups need it (and 2FA still on). */
|
|
30
|
+
readonly secondFactor: boolean;
|
|
31
|
+
readonly via: 'cookie' | 'bearer';
|
|
32
|
+
}
|
|
33
|
+
/** A user's sign-in state as `/auth/me` returns it. */
|
|
34
|
+
export interface UserInfo {
|
|
35
|
+
readonly id: number;
|
|
36
|
+
readonly login: string;
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly company: number | null;
|
|
39
|
+
readonly companies: readonly number[];
|
|
40
|
+
readonly twoFactorEnabled: boolean;
|
|
41
|
+
/** Holds an administrator role (in some company). */
|
|
42
|
+
readonly administrator: boolean;
|
|
43
|
+
/** Administrator rights are withheld until this session has a second factor (enable or verify 2FA). */
|
|
44
|
+
readonly administratorNeedsSecondFactor: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare const SESSION_COOKIE = "kosh_session";
|
|
47
|
+
/** Milliseconds of a datetime value as Shutro returns it (`Date`, or an ISO text), or `null`. */
|
|
48
|
+
export declare function timeOf(value: unknown): number | null;
|
|
49
|
+
/** Reads `name` from the Cookie header. */
|
|
50
|
+
export declare function cookieOf(request: FastifyRequest, name: string): string | undefined;
|
|
51
|
+
/** The authentication service of one server. */
|
|
52
|
+
export declare class AuthService {
|
|
53
|
+
#private;
|
|
54
|
+
constructor(shutro: () => Shutro, options?: AuthOptions);
|
|
55
|
+
/** The `Set-Cookie` value for a session token (empty token: clears it). */
|
|
56
|
+
cookie(token: string | null): string;
|
|
57
|
+
/** Hashes and sets a user's password. @throws {HttpProblem} 422 for a too-short password */
|
|
58
|
+
setPassword(env: Env, user: number, password: string): Promise<void>;
|
|
59
|
+
/** Creates a user with a password and group assignments (installation, tests, admin tools). */
|
|
60
|
+
createUser(input: {
|
|
61
|
+
readonly login: string;
|
|
62
|
+
readonly name: string;
|
|
63
|
+
readonly password: string;
|
|
64
|
+
readonly company?: number;
|
|
65
|
+
readonly companies?: readonly number[];
|
|
66
|
+
/** Group codes, all companies (`company` omitted) or per company. */
|
|
67
|
+
readonly groups?: readonly (string | {
|
|
68
|
+
readonly group: string;
|
|
69
|
+
readonly company: number;
|
|
70
|
+
})[];
|
|
71
|
+
}): Promise<number>;
|
|
72
|
+
/** Signs in: a new session (pending second factor when 2FA is on). @throws {HttpProblem} 401 */
|
|
73
|
+
login(request: FastifyRequest, login: string, password: string): Promise<{
|
|
74
|
+
token: string;
|
|
75
|
+
csrfToken: string;
|
|
76
|
+
twoFactorRequired: boolean;
|
|
77
|
+
}>;
|
|
78
|
+
/** The request's session, or `null`. Cookie sessions need the CSRF header on unsafe methods. */
|
|
79
|
+
session(request: FastifyRequest): Promise<SessionInfo | null>;
|
|
80
|
+
/** The verified session of a request. @throws {HttpProblem} 401 without one, or before the second factor */
|
|
81
|
+
requireSession(request: FastifyRequest, options?: {
|
|
82
|
+
readonly allowPending?: boolean;
|
|
83
|
+
}): Promise<SessionInfo>;
|
|
84
|
+
/** Authenticator for the server (RFC 0002 §6.2): the session's user, company from `X-Company`, groups for it. */
|
|
85
|
+
authenticate(request: FastifyRequest): Promise<RequestIdentity | null>;
|
|
86
|
+
/** Identity loader for Shutro (`createShutro({ identity })`): the user is active; companies and groups from the database. */
|
|
87
|
+
verify(uid: number): Promise<VerifiedIdentity | null>;
|
|
88
|
+
/** The signed-in user's state. */
|
|
89
|
+
me(session: SessionInfo): Promise<UserInfo>;
|
|
90
|
+
/** Revokes a session. */
|
|
91
|
+
logout(session: SessionInfo): Promise<void>;
|
|
92
|
+
/** Starts 2FA enrolment: a new secret, not active until confirmed with a code. */
|
|
93
|
+
setupTwoFactor(session: SessionInfo): Promise<{
|
|
94
|
+
secret: string;
|
|
95
|
+
otpauthUri: string;
|
|
96
|
+
}>;
|
|
97
|
+
/** Confirms enrolment with a code; returns ten recovery codes (shown once, stored as digests). */
|
|
98
|
+
enableTwoFactor(session: SessionInfo, code: string): Promise<{
|
|
99
|
+
recoveryCodes: string[];
|
|
100
|
+
}>;
|
|
101
|
+
/** Completes a pending sign-in with a TOTP or recovery code (a recovery code works once). */
|
|
102
|
+
verifyTwoFactor(session: SessionInfo, code: string): Promise<void>;
|
|
103
|
+
/** Turns 2FA off (password and a current code required). */
|
|
104
|
+
disableTwoFactor(session: SessionInfo, password: string, code: string): Promise<void>;
|
|
105
|
+
/** Changes the password; every other session of the user is revoked. */
|
|
106
|
+
changePassword(session: SessionInfo, current: string, next: string): Promise<void>;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/auth/service.ts"],"names":[],"mappings":"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"}
|