@nacre.work/api 0.0.0 → 0.2.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.
Files changed (69) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/dist/adapters.d.ts +421 -17
  4. package/dist/adapters.d.ts.map +1 -1
  5. package/dist/adapters.js +1092 -72
  6. package/dist/adapters.js.map +1 -1
  7. package/dist/audit-export.d.ts +45 -0
  8. package/dist/audit-export.d.ts.map +1 -0
  9. package/dist/audit-export.js +155 -0
  10. package/dist/audit-export.js.map +1 -0
  11. package/dist/auth.d.ts +17 -0
  12. package/dist/auth.d.ts.map +1 -1
  13. package/dist/auth.js +17 -7
  14. package/dist/auth.js.map +1 -1
  15. package/dist/idempotency.d.ts +62 -0
  16. package/dist/idempotency.d.ts.map +1 -0
  17. package/dist/idempotency.js +147 -0
  18. package/dist/idempotency.js.map +1 -0
  19. package/dist/index.d.ts +12 -3
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +10 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/init.d.ts +9 -0
  24. package/dist/init.d.ts.map +1 -1
  25. package/dist/init.js +50 -5
  26. package/dist/init.js.map +1 -1
  27. package/dist/limits.d.ts +80 -0
  28. package/dist/limits.d.ts.map +1 -0
  29. package/dist/limits.js +63 -0
  30. package/dist/limits.js.map +1 -0
  31. package/dist/login.d.ts +94 -0
  32. package/dist/login.d.ts.map +1 -0
  33. package/dist/login.js +283 -0
  34. package/dist/login.js.map +1 -0
  35. package/dist/main.js +171 -60
  36. package/dist/main.js.map +1 -1
  37. package/dist/pagination.d.ts +72 -0
  38. package/dist/pagination.d.ts.map +1 -0
  39. package/dist/pagination.js +110 -0
  40. package/dist/pagination.js.map +1 -0
  41. package/dist/rerank.d.ts +99 -0
  42. package/dist/rerank.d.ts.map +1 -0
  43. package/dist/rerank.js +138 -0
  44. package/dist/rerank.js.map +1 -0
  45. package/dist/server.d.ts +361 -6
  46. package/dist/server.d.ts.map +1 -1
  47. package/dist/server.js +1050 -31
  48. package/dist/server.js.map +1 -1
  49. package/dist/service-keys.d.ts +13 -5
  50. package/dist/service-keys.d.ts.map +1 -1
  51. package/dist/service-keys.js +54 -31
  52. package/dist/service-keys.js.map +1 -1
  53. package/dist/source.d.ts +53 -0
  54. package/dist/source.d.ts.map +1 -0
  55. package/dist/source.js +109 -0
  56. package/dist/source.js.map +1 -0
  57. package/package.json +28 -10
  58. package/dist/__tests__/admin-surface.test.d.ts +0 -2
  59. package/dist/__tests__/admin-surface.test.d.ts.map +0 -1
  60. package/dist/__tests__/admin-surface.test.js +0 -431
  61. package/dist/__tests__/admin-surface.test.js.map +0 -1
  62. package/dist/__tests__/init.test.d.ts +0 -2
  63. package/dist/__tests__/init.test.d.ts.map +0 -1
  64. package/dist/__tests__/init.test.js +0 -125
  65. package/dist/__tests__/init.test.js.map +0 -1
  66. package/dist/__tests__/service-keys.test.d.ts +0 -2
  67. package/dist/__tests__/service-keys.test.d.ts.map +0 -1
  68. package/dist/__tests__/service-keys.test.js +0 -204
  69. package/dist/__tests__/service-keys.test.js.map +0 -1
package/dist/login.js ADDED
@@ -0,0 +1,283 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { hashPassword, needsRehash, spendVerificationTime, verifyPassword, whileAuthenticating, withOrg, } from '@nacre.work/core';
3
+ import { SignJWT } from 'jose';
4
+ /**
5
+ * Email and password sign-in.
6
+ *
7
+ * The last thing in `docs/api.md` that was described and not built. Until now
8
+ * the only credentials were a token printed by `init` — valid for an hour,
9
+ * signed with a symmetric secret, and printed into somebody's shell history —
10
+ * and service account keys, which are for programs.
11
+ *
12
+ * SSO is a commercial module. This is the one every self-hoster gets, which is
13
+ * why it is here rather than there.
14
+ *
15
+ * ## Invariant I1 at the one moment there is no token
16
+ *
17
+ * "The organization comes from the token, never from a body" governs
18
+ * authenticated requests. Login is the request that has no token yet, and the
19
+ * rule still shapes it: the body may name an organization, but only as part of
20
+ * the lookup key. **What ends up in the token is the `org_id` on the row that
21
+ * authenticated**, never the string the caller sent. A caller who names one
22
+ * organization and holds a password in another gets a refusal, not a token for
23
+ * either.
24
+ *
25
+ * The slug is optional because a single-organization installation — which is
26
+ * what the open core is — should not make people type it. Omitted, the email
27
+ * has to match exactly one user in the whole installation; zero and several
28
+ * are the same refusal, so this cannot be used to ask how many organizations
29
+ * an address appears in.
30
+ *
31
+ * ## One refusal, always
32
+ *
33
+ * Unknown address, wrong password, wrong organization, disabled account, an
34
+ * account with no password set: all `401`, same wording, same time spent. The
35
+ * timing is why `spendVerificationTime` exists — an early return on "no such
36
+ * user" makes the response time an oracle for which addresses have accounts,
37
+ * and that would undo the care taken everywhere else about not confirming what
38
+ * exists.
39
+ */
40
+ const ACCESS_MIN_SECONDS = 60;
41
+ const digest = (token) => createHash('sha256').update(token).digest('hex');
42
+ /** 32 bytes from the CSPRNG. Opaque — it is looked up, never parsed. */
43
+ const mint = () => randomBytes(32).toString('base64url');
44
+ export class Login {
45
+ deps;
46
+ constructor(deps) {
47
+ this.deps = deps;
48
+ }
49
+ get scope() {
50
+ return this.deps.role === undefined ? {} : { role: this.deps.role };
51
+ }
52
+ get clock() {
53
+ return this.deps.now?.() ?? new Date();
54
+ }
55
+ /**
56
+ * Sign in, or refuse.
57
+ *
58
+ * `undefined` for every reason it can fail. The caller turns it into one
59
+ * `401` with one message.
60
+ */
61
+ async login(request) {
62
+ const email = request.email.trim().toLowerCase();
63
+ if (email === '' || request.password === '') {
64
+ // Still spend the time. A short-circuit on an empty field is a free probe
65
+ // for how long the real path takes.
66
+ await spendVerificationTime(request.password);
67
+ return undefined;
68
+ }
69
+ const candidate = await this.find(email, request.organization);
70
+ if (candidate === undefined || candidate.password_hash === null || candidate.disabled_at !== null) {
71
+ await spendVerificationTime(request.password);
72
+ return undefined;
73
+ }
74
+ if (!(await verifyPassword(request.password, candidate.password_hash)))
75
+ return undefined;
76
+ // The only moment the plaintext exists, so the only moment a stored hash
77
+ // made with weaker parameters can be brought up to date.
78
+ if (needsRehash(candidate.password_hash)) {
79
+ const rehashed = await hashPassword(request.password);
80
+ await withOrg(this.deps.pool, candidate.org_id, (client) => client.query('UPDATE users SET password_hash = $1 WHERE id = $2', [rehashed, candidate.id]), this.scope).catch(() => undefined);
81
+ }
82
+ return this.issue(candidate, undefined);
83
+ }
84
+ /**
85
+ * Find the one user this login is about, without saying how many nearly were.
86
+ *
87
+ * `organizations` carries no row-level security — an organization is not
88
+ * inside one — so the slug resolves on an ordinary connection, and the user
89
+ * is then read through `withOrg` like everything else. That is why the login
90
+ * path needs no cross-tenant read and no escape hatch: it is two scoped
91
+ * queries rather than one unscoped one.
92
+ */
93
+ async find(email, organization) {
94
+ if (organization !== undefined && organization.trim() !== '') {
95
+ const orgId = await this.orgIdForSlug(organization.trim().toLowerCase());
96
+ if (orgId === undefined)
97
+ return undefined;
98
+ return this.userIn(orgId, email);
99
+ }
100
+ // No slug. Which is the ordinary case for the single-organization
101
+ // installation the open core is, and has to stay silent about how many
102
+ // organizations exist when it is not.
103
+ const orgIds = await this.allOrgIds();
104
+ const found = [];
105
+ for (const orgId of orgIds) {
106
+ const row = await this.userIn(orgId, email);
107
+ if (row !== undefined)
108
+ found.push(row);
109
+ // Two is already ambiguous; there is nothing to learn from a third, and
110
+ // stopping keeps the cost of a login independent of how many tenants a
111
+ // deployment has.
112
+ if (found.length > 1)
113
+ return undefined;
114
+ }
115
+ return found[0];
116
+ }
117
+ async orgIdForSlug(slug) {
118
+ const client = await this.deps.pool.connect();
119
+ try {
120
+ const { rows } = await client.query('SELECT id FROM organizations WHERE slug = $1 AND deleted_at IS NULL', [slug]);
121
+ return rows[0]?.id;
122
+ }
123
+ finally {
124
+ client.release();
125
+ }
126
+ }
127
+ async allOrgIds() {
128
+ const client = await this.deps.pool.connect();
129
+ try {
130
+ const { rows } = await client.query('SELECT id FROM organizations WHERE deleted_at IS NULL');
131
+ return rows.map((r) => r.id);
132
+ }
133
+ finally {
134
+ client.release();
135
+ }
136
+ }
137
+ async userIn(orgId, email) {
138
+ return withOrg(this.deps.pool, orgId, async (client) => {
139
+ const { rows } = await client.query(`SELECT id, org_id, role, password_hash, disabled_at
140
+ FROM users WHERE org_id = $1 AND email = $2`, [orgId, email]);
141
+ return rows[0];
142
+ }, this.scope);
143
+ }
144
+ /**
145
+ * Exchange a refresh token for a new pair, or refuse.
146
+ *
147
+ * Rotation on every use. A token presented after it has been used is not
148
+ * tolerated as a client retry: the legitimate holder already exchanged it, so
149
+ * a second presentation means two parties hold the same token and one of them
150
+ * took it. Which one is unknowable from here, so the whole family is revoked
151
+ * and both sign in again.
152
+ */
153
+ async refresh(token) {
154
+ const row = await whileAuthenticating(this.deps.pool, async (client) => {
155
+ const { rows } = await client.query(`SELECT id, org_id, user_id, family_id, expires_at, used_at, revoked_at, token_hash
156
+ FROM refresh_tokens WHERE token_hash = $1`, [digest(token)]);
157
+ return rows[0];
158
+ }, this.scope);
159
+ if (row === undefined)
160
+ return undefined;
161
+ // Constant-time on the hash as well. The lookup already matched it, so this
162
+ // guards only against a future where the query becomes a prefix match — but
163
+ // that is exactly the change that would otherwise turn this into an oracle.
164
+ const presented = Buffer.from(digest(token), 'utf8');
165
+ const stored = Buffer.from(row.token_hash, 'utf8');
166
+ if (presented.length !== stored.length || !timingSafeEqual(presented, stored))
167
+ return undefined;
168
+ if (row.revoked_at !== null)
169
+ return undefined;
170
+ if (row.expires_at.getTime() <= this.clock.getTime())
171
+ return undefined;
172
+ // Everything from here is one transaction, serialized on the family.
173
+ //
174
+ // The read above is diagnosis, not the decision. Checking `used_at` in
175
+ // application code and writing it later is a read-modify-write, and under
176
+ // concurrency every request reads null before any of them writes: eight
177
+ // simultaneous redemptions of one stolen token all succeeded, each with a
178
+ // fresh session, and the reuse detection never fired. Rotation without an
179
+ // atomic claim detects nothing — it only makes theft quieter.
180
+ //
181
+ // `used_at IS NULL` in the WHERE clause is the claim, and the advisory lock
182
+ // is what makes the revocation that follows a loss complete: without it a
183
+ // revoke whose statement began before this transaction committed cannot see
184
+ // the row inserted here, so the token the winner just obtained would
185
+ // survive the revocation meant to catch it.
186
+ const outcome = await withOrg(this.deps.pool, row.org_id, async (client) => {
187
+ await client.query('SELECT pg_advisory_xact_lock(hashtext($1))', [row.family_id]);
188
+ const claimed = await client.query(`UPDATE refresh_tokens SET used_at = now()
189
+ WHERE id = $1 AND used_at IS NULL AND revoked_at IS NULL
190
+ RETURNING id`, [row.id]);
191
+ if (claimed.rows[0] === undefined)
192
+ return 'lost';
193
+ const { rows } = await client.query(`SELECT id, org_id, role, password_hash, disabled_at
194
+ FROM users WHERE id = $1 AND org_id = $2`, [row.user_id, row.org_id]);
195
+ const user = rows[0];
196
+ if (user === undefined || user.disabled_at !== null)
197
+ return 'gone';
198
+ return this.issue(user, row.family_id, client);
199
+ }, this.scope);
200
+ if (outcome === 'lost') {
201
+ // Someone already spent this one. Two parties hold the same token and
202
+ // there is no way to tell which is genuine, so the session ends for both.
203
+ await this.revokeFamily(row.org_id, row.family_id);
204
+ return undefined;
205
+ }
206
+ if (outcome === 'gone') {
207
+ // Disabling an account has to end its sessions, or it only stops new
208
+ // ones. The claim above is committed, so the token is spent either way —
209
+ // a refusal here must not leave it redeemable.
210
+ await this.revokeFamily(row.org_id, row.family_id);
211
+ return undefined;
212
+ }
213
+ return outcome;
214
+ }
215
+ /** Sign out. Ends the family, not just this token. */
216
+ async logout(token) {
217
+ const row = await whileAuthenticating(this.deps.pool, async (client) => {
218
+ const { rows } = await client.query('SELECT org_id, family_id FROM refresh_tokens WHERE token_hash = $1 AND revoked_at IS NULL', [digest(token)]);
219
+ return rows[0];
220
+ }, this.scope);
221
+ if (row === undefined)
222
+ return false;
223
+ await this.revokeFamily(row.org_id, row.family_id);
224
+ return true;
225
+ }
226
+ /**
227
+ * End every token descended from one login.
228
+ *
229
+ * Takes the same advisory lock the rotation path takes, and that is what
230
+ * makes it complete rather than approximately complete. Without it the two
231
+ * interleave badly: a statement's snapshot is fixed when the statement
232
+ * starts, so a revoke that begins before a rotation commits will not see the
233
+ * row that rotation inserts — and the token the racing party just obtained
234
+ * survives the revocation that was supposed to catch it. Serialized on the
235
+ * family, the revoke either runs first (and the rotation then finds the
236
+ * family revoked) or runs second (and sees the new row).
237
+ */
238
+ async revokeFamily(orgId, familyId) {
239
+ await withOrg(this.deps.pool, orgId, async (client) => {
240
+ await client.query('SELECT pg_advisory_xact_lock(hashtext($1))', [familyId]);
241
+ await client.query('UPDATE refresh_tokens SET revoked_at = now() WHERE family_id = $1 AND revoked_at IS NULL', [familyId]);
242
+ }, this.scope);
243
+ }
244
+ /**
245
+ * Mint an access token and the refresh token that succeeds it.
246
+ *
247
+ * `family` carries across a rotation so the whole chain from one login can be
248
+ * revoked together; a fresh login starts a new one, so signing out of one
249
+ * device does not sign out of the others.
250
+ */
251
+ async issue(user, family, client) {
252
+ const ttl = Math.max(ACCESS_MIN_SECONDS, this.deps.accessTokenTtl);
253
+ const now = this.clock;
254
+ const accessToken = await new SignJWT({
255
+ // From the row that authenticated, never from the request. This is the
256
+ // line invariant I1 is about.
257
+ org: user.org_id,
258
+ principal_type: 'user',
259
+ role: user.role,
260
+ })
261
+ .setProtectedHeader({ alg: 'HS256' })
262
+ .setSubject(user.id)
263
+ .setIssuer(this.deps.issuer)
264
+ .setAudience(this.deps.audience)
265
+ .setIssuedAt(Math.floor(now.getTime() / 1000))
266
+ .setExpirationTime(Math.floor(now.getTime() / 1000) + ttl)
267
+ .sign(this.deps.key);
268
+ const refreshToken = mint();
269
+ const expiresAt = new Date(now.getTime() + this.deps.refreshTokenTtl * 1000);
270
+ const insert = (client) => client.query(`INSERT INTO refresh_tokens (org_id, user_id, token_hash, family_id, expires_at)
271
+ VALUES ($1, $2, $3, COALESCE($4::uuid, gen_random_uuid()), $5)`, [user.org_id, user.id, digest(refreshToken), family ?? null, expiresAt]);
272
+ // On a rotation the caller hands in its own transaction, so the claim and
273
+ // the token that replaces it commit together — a crash between them would
274
+ // otherwise spend a token and issue nothing, ending a session for no
275
+ // reason. A fresh login has no transaction to join and opens its own.
276
+ if (client !== undefined)
277
+ await insert(client);
278
+ else
279
+ await withOrg(this.deps.pool, user.org_id, insert, this.scope);
280
+ return { accessToken, refreshToken, expiresIn: ttl, orgId: user.org_id, userId: user.id };
281
+ }
282
+ }
283
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../src/login.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAEtE,OAAO,EACL,YAAY,EACZ,WAAW,EACX,qBAAqB,EACrB,cAAc,EACd,mBAAmB,EACnB,OAAO,GACR,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAG9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,MAAM,kBAAkB,GAAG,EAAE,CAAA;AA8C7B,MAAM,MAAM,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAE1F,wEAAwE;AACxE,MAAM,IAAI,GAAG,GAAW,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;AAEhE,MAAM,OAAO,KAAK;IACa;IAA7B,YAA6B,IAAe;QAAf,SAAI,GAAJ,IAAI,CAAW;IAAG,CAAC;IAEhD,IAAY,KAAK;QACf,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;IACrE,CAAC;IAED,IAAY,KAAK;QACf,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,IAAI,IAAI,EAAE,CAAA;IACxC,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,KAAK,CAAC,OAAqB;QAC/B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;QAChD,IAAI,KAAK,KAAK,EAAE,IAAI,OAAO,CAAC,QAAQ,KAAK,EAAE,EAAE,CAAC;YAC5C,0EAA0E;YAC1E,oCAAoC;YACpC,MAAM,qBAAqB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;YAC7C,OAAO,SAAS,CAAA;QAClB,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,CAAA;QAC9D,IAAI,SAAS,KAAK,SAAS,IAAI,SAAS,CAAC,aAAa,KAAK,IAAI,IAAI,SAAS,CAAC,WAAW,KAAK,IAAI,EAAE,CAAC;YAClG,MAAM,qBAAqB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;YAC7C,OAAO,SAAS,CAAA;QAClB,CAAC;QAED,IAAI,CAAC,CAAC,MAAM,cAAc,CAAC,OAAO,CAAC,QAAQ,EAAE,SAAS,CAAC,aAAa,CAAC,CAAC;YAAE,OAAO,SAAS,CAAA;QAExF,yEAAyE;QACzE,yDAAyD;QACzD,IAAI,WAAW,CAAC,SAAS,CAAC,aAAa,CAAC,EAAE,CAAC;YACzC,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;YACrD,MAAM,OAAO,CACX,IAAI,CAAC,IAAI,CAAC,IAAI,EACd,SAAS,CAAC,MAAM,EAChB,CAAC,MAAM,EAAE,EAAE,CACT,MAAM,CAAC,KAAK,CAAC,mDAAmD,EAAE,CAAC,QAAQ,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC,EAC7F,IAAI,CAAC,KAAK,CACX,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;QAC1B,CAAC;QAED,OAAO,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;IACzC,CAAC;IAED;;;;;;;;OAQG;IACK,KAAK,CAAC,IAAI,CAAC,KAAa,EAAE,YAAgC;QAChE,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YAC7D,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAA;YACxE,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO,SAAS,CAAA;YACzC,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;QAClC,CAAC;QAED,kEAAkE;QAClE,uEAAuE;QACvE,sCAAsC;QACtC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAA;QACrC,MAAM,KAAK,GAAc,EAAE,CAAA;QAC3B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;YAC3C,IAAI,GAAG,KAAK,SAAS;gBAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YACtC,wEAAwE;YACxE,uEAAuE;YACvE,kBAAkB;YAClB,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;gBAAE,OAAO,SAAS,CAAA;QACxC,CAAC;QACD,OAAO,KAAK,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAEO,KAAK,CAAC,YAAY,CAAC,IAAY;QACrC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAA;QAC7C,IAAI,CAAC;YACH,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,KAAK,CACjC,qEAAqE,EACrE,CAAC,IAAI,CAAC,CACP,CAAA;YACD,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAA;QACpB,CAAC;gBAAS,CAAC;YACT,MAAM,CAAC,OAAO,EAAE,CAAA;QAClB,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,SAAS;QACrB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAA;QAC7C,IAAI,CAAC;YACH,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,KAAK,CACjC,uDAAuD,CACxD,CAAA;YACD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;QAC9B,CAAC;gBAAS,CAAC;YACT,MAAM,CAAC,OAAO,EAAE,CAAA;QAClB,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,MAAM,CAAC,KAAa,EAAE,KAAa;QAC/C,OAAO,OAAO,CACZ,IAAI,CAAC,IAAI,CAAC,IAAI,EACd,KAAK,EACL,KAAK,EAAE,MAAM,EAAE,EAAE;YACf,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,KAAK,CACjC;yDAC+C,EAC/C,CAAC,KAAK,EAAE,KAAK,CAAC,CACf,CAAA;YACD,OAAO,IAAI,CAAC,CAAC,CAAC,CAAA;QAChB,CAAC,EACD,IAAI,CAAC,KAAK,CACX,CAAA;IACH,CAAC;IAED;;;;;;;;OAQG;IACH,KAAK,CAAC,OAAO,CAAC,KAAa;QACzB,MAAM,GAAG,GAAG,MAAM,mBAAmB,CACnC,IAAI,CAAC,IAAI,CAAC,IAAI,EACd,KAAK,EAAE,MAAM,EAAE,EAAE;YACf,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,KAAK,CAUjC;uDAC6C,EAC7C,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAChB,CAAA;YACD,OAAO,IAAI,CAAC,CAAC,CAAC,CAAA;QAChB,CAAC,EACD,IAAI,CAAC,KAAK,CACX,CAAA;QAED,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,SAAS,CAAA;QAEvC,4EAA4E;QAC5E,4EAA4E;QAC5E,4EAA4E;QAC5E,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAA;QACpD,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;QAClD,IAAI,SAAS,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,MAAM,CAAC;YAAE,OAAO,SAAS,CAAA;QAE/F,IAAI,GAAG,CAAC,UAAU,KAAK,IAAI;YAAE,OAAO,SAAS,CAAA;QAE7C,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE;YAAE,OAAO,SAAS,CAAA;QAEtE,qEAAqE;QACrE,EAAE;QACF,uEAAuE;QACvE,0EAA0E;QAC1E,wEAAwE;QACxE,0EAA0E;QAC1E,0EAA0E;QAC1E,8DAA8D;QAC9D,EAAE;QACF,4EAA4E;QAC5E,0EAA0E;QAC1E,4EAA4E;QAC5E,qEAAqE;QACrE,4CAA4C;QAC5C,MAAM,OAAO,GAAG,MAAM,OAAO,CAC3B,IAAI,CAAC,IAAI,CAAC,IAAI,EACd,GAAG,CAAC,MAAM,EACV,KAAK,EAAE,MAAM,EAAqC,EAAE;YAClD,MAAM,MAAM,CAAC,KAAK,CAAC,4CAA4C,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAA;YAEjF,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,KAAK,CAChC;;yBAEe,EACf,CAAC,GAAG,CAAC,EAAE,CAAC,CACT,CAAA;YACD,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS;gBAAE,OAAO,MAAM,CAAA;YAEhD,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,KAAK,CACjC;sDAC4C,EAC5C,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAC1B,CAAA;YACD,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;YACpB,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI;gBAAE,OAAO,MAAM,CAAA;YAElE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;QAChD,CAAC,EACD,IAAI,CAAC,KAAK,CACX,CAAA;QAED,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;YACvB,sEAAsE;YACtE,0EAA0E;YAC1E,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,SAAS,CAAC,CAAA;YAClD,OAAO,SAAS,CAAA;QAClB,CAAC;QAED,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;YACvB,qEAAqE;YACrE,yEAAyE;YACzE,+CAA+C;YAC/C,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,SAAS,CAAC,CAAA;YAClD,OAAO,SAAS,CAAA;QAClB,CAAC;QAED,OAAO,OAAO,CAAA;IAChB,CAAC;IAED,sDAAsD;IACtD,KAAK,CAAC,MAAM,CAAC,KAAa;QACxB,MAAM,GAAG,GAAG,MAAM,mBAAmB,CACnC,IAAI,CAAC,IAAI,CAAC,IAAI,EACd,KAAK,EAAE,MAAM,EAAE,EAAE;YACf,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,KAAK,CACjC,2FAA2F,EAC3F,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAChB,CAAA;YACD,OAAO,IAAI,CAAC,CAAC,CAAC,CAAA;QAChB,CAAC,EACD,IAAI,CAAC,KAAK,CACX,CAAA;QAED,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,KAAK,CAAA;QACnC,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,SAAS,CAAC,CAAA;QAClD,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;;;;;;;;;OAWG;IACK,KAAK,CAAC,YAAY,CAAC,KAAa,EAAE,QAAgB;QACxD,MAAM,OAAO,CACX,IAAI,CAAC,IAAI,CAAC,IAAI,EACd,KAAK,EACL,KAAK,EAAE,MAAM,EAAE,EAAE;YACf,MAAM,MAAM,CAAC,KAAK,CAAC,4CAA4C,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAA;YAC5E,MAAM,MAAM,CAAC,KAAK,CAChB,0FAA0F,EAC1F,CAAC,QAAQ,CAAC,CACX,CAAA;QACH,CAAC,EACD,IAAI,CAAC,KAAK,CACX,CAAA;IACH,CAAC;IAED;;;;;;OAMG;IACK,KAAK,CAAC,KAAK,CACjB,IAAa,EACb,MAA0B,EAC1B,MAAmB;QAEnB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,kBAAkB,EAAE,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;QAClE,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAA;QAEtB,MAAM,WAAW,GAAG,MAAM,IAAI,OAAO,CAAC;YACpC,uEAAuE;YACvE,8BAA8B;YAC9B,GAAG,EAAE,IAAI,CAAC,MAAM;YAChB,cAAc,EAAE,MAAM;YACtB,IAAI,EAAE,IAAI,CAAC,IAAI;SAChB,CAAC;aACC,kBAAkB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;aACpC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;aACnB,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;aAC3B,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;aAC/B,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC;aAC7C,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC;aACzD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAEtB,MAAM,YAAY,GAAG,IAAI,EAAE,CAAA;QAC3B,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,CAAA;QAE5E,MAAM,MAAM,GAAG,CAAC,MAAkB,EAAoB,EAAE,CACtD,MAAM,CAAC,KAAK,CACV;wEACgE,EAChE,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,CAAC,YAAY,CAAC,EAAE,MAAM,IAAI,IAAI,EAAE,SAAS,CAAC,CACxE,CAAA;QAEH,0EAA0E;QAC1E,0EAA0E;QAC1E,qEAAqE;QACrE,sEAAsE;QACtE,IAAI,MAAM,KAAK,SAAS;YAAE,MAAM,MAAM,CAAC,MAAM,CAAC,CAAA;;YACzC,MAAM,OAAO,CAChB,IAAI,CAAC,IAAI,CAAC,IAAI,EACd,IAAI,CAAC,MAAM,EACX,MAAM,EACN,IAAI,CAAC,KAAK,CACX,CAAA;QAED,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAAA;IAC3F,CAAC;CACF"}
package/dist/main.js CHANGED
@@ -1,6 +1,10 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { collectDatabaseGauges, createMetrics, createPool, loadConfig, Registry, VectorStore, vectorName, withOrg, ConfigError, } from '@nacre.work/core';
3
- import { HttpEmbedder, NacreIngest, NacreSearchService, PostgresAudit, PostgresDocuments, PostgresGrants, PostgresJobs, PostgresLayers, } from './adapters.js';
2
+ import { configureLogging, collectDatabaseGauges, createMetrics, createPool, loadConfig, logger, loadJwtKeys, protectedResourceMetadata, Redis, RedisCache, Registry, S3, VectorStore, withOrg, ConfigError, installGuards, onListenError, } from '@nacre.work/core';
3
+ import { HttpEmbedder, NacreIngest, NacreSearchService, PostgresAudit, PostgresAuditReader, PostgresDocuments, PostgresGrants, PostgresJobs, PostgresLayers, PostgresReindex, PostgresWorkspaces, } from './adapters.js';
4
+ import { Idempotency } from './idempotency.js';
5
+ import { Login } from './login.js';
6
+ import { rerankerFor } from './rerank.js';
7
+ import { RateLimiter } from './limits.js';
4
8
  import { PostgresServiceAccounts, PostgresServiceKeys } from './service-keys.js';
5
9
  import { createApi } from './server.js';
6
10
  /**
@@ -11,103 +15,210 @@ import { createApi } from './server.js';
11
15
  * below this line has a fallback.
12
16
  */
13
17
  const APP_ROLE = 'nacre_app';
14
- function jwtKey(config) {
15
- // Development uses a symmetric secret. Production is meant to load an Ed25519
16
- // key through NACRE_JWT_PRIVATE_KEY_REF; until that lands, refusing is the
17
- // honest behaviour — a hardcoded fallback here would be a signing key anyone
18
- // reading the source can forge tokens with.
19
- const secret = process.env.NACRE_JWT_SECRET;
20
- if (secret === undefined || secret.length < 32) {
21
- throw new ConfigError([
22
- 'NACRE_JWT_SECRET is not set, or is shorter than 32 bytes. ' +
23
- 'Asymmetric keys through NACRE_JWT_PRIVATE_KEY_REF are not implemented yet; ' +
24
- 'until they are, this is required and there is no default.',
25
- ]);
26
- }
27
- void config;
28
- return new TextEncoder().encode(secret);
29
- }
30
18
  async function main() {
31
19
  const config = loadConfig();
32
- const key = jwtKey({});
20
+ // Before anything else logs. `NACRE_LOG_LEVEL` and `NACRE_LOG_FORMAT` had been
21
+ // validated here and read by nothing, so every process wrote JSON at one level
22
+ // whatever the deployment asked for.
23
+ configureLogging({ level: config.logLevel, format: config.logFormat });
24
+ const { key, alsoAccept } = loadJwtKeys();
33
25
  const pool = createPool({ connectionString: config.pgUrl, max: config.pgPoolMax });
34
26
  const vectors = new VectorStore(config.qdrantApiKey === undefined
35
27
  ? { url: config.qdrantUrl }
36
28
  : { url: config.qdrantUrl, apiKey: config.qdrantApiKey });
37
- const embedder = new HttpEmbedder(config.embeddingEndpoint, config.embeddingModel, config.embeddingDim);
38
- // Slugs change rarely and are read on every search. A tiny cache keyed on the
39
- // organization keeps a search from costing an extra round trip; it is not
40
- // permission data, so staleness here cannot widen access.
41
- const slugs = new Map();
42
- const orgSlug = async (orgId) => {
43
- const cached = slugs.get(orgId);
44
- if (cached !== undefined)
45
- return cached;
46
- const found = await withOrg(pool, orgId, async (client) => {
47
- const { rows } = await client.query('SELECT slug FROM organizations WHERE id = $1 AND deleted_at IS NULL', [orgId]);
48
- return rows[0]?.slug;
49
- }, { role: APP_ROLE });
50
- if (found !== undefined)
51
- slugs.set(orgId, found);
52
- return found;
53
- };
29
+ // Absent when a deployment has none, which is supported and is the default:
30
+ // document bytes then live in `documents.source_ref`. `loadConfig` refuses a
31
+ // half-configured block, so this is either fully usable or not there at all.
32
+ const objects = config.s3 === undefined ? undefined : new S3(config.s3);
54
33
  const registry = new Registry();
55
34
  const metrics = createMetrics(registry);
56
35
  registry.collect(collectDatabaseGauges(pool, metrics, APP_ROLE));
36
+ // Redis has been required configuration, and in every Compose profile with
37
+ // the API waiting on its healthcheck, since before anything connected to it.
38
+ // Rate limiting and Idempotency-Key are what it was declared for.
39
+ const redis = new Redis({ url: config.redisUrl });
40
+ // The effective-principals cache, shared with MCP through Redis so both
41
+ // surfaces answer from one place. `NACRE_ACL_CACHE_TTL` had been validated at
42
+ // startup and read by nothing since it was added: the cache existed, was
43
+ // tested, and was never called, so every request recomputed the transitive
44
+ // group closure.
45
+ //
46
+ // Safe to cache because the key carries `organizations.groups_version`, which
47
+ // triggers bump on every change to groups, group_members and grants — a
48
+ // revoked grant is not served stale, the next request simply looks somewhere
49
+ // else. The TTL bounds memory, not correctness.
50
+ const principalsCache = { store: new RedisCache(redis), ttlSeconds: config.aclCacheTtl };
51
+ const limitPolicies = {
52
+ search: { limit: config.rateSearchPerMin, windowSeconds: 60 },
53
+ ingest: { limit: config.rateIngestPerHour, windowSeconds: 3600 },
54
+ // Per address rather than per organization — there is no organization until
55
+ // the sign-in succeeds, and what this defends is one account's password.
56
+ login: { limit: config.rateLoginPer15Min, windowSeconds: 900 },
57
+ // And per client, which is what the address limit does not do: a spray
58
+ // across ten thousand addresses never repeats a key. Looser on purpose,
59
+ // because a whole office behind one NAT is one source here.
60
+ login_source: { limit: config.rateLoginSourcePer15Min, windowSeconds: 900 },
61
+ };
62
+ const limits = new RateLimiter({
63
+ redis,
64
+ policies: limitPolicies,
65
+ onDegraded: (resource, error) => {
66
+ // Allowed through, and said so. A rate limit is availability protection
67
+ // rather than an authorization control, so failing closed here would
68
+ // trade a rare over-serve for a certain outage — the opposite of the
69
+ // rule for permissions, and deliberately so.
70
+ logger.warn('rate limit check unavailable; request allowed', { resource,
71
+ error: String(error).slice(0, 200) });
72
+ },
73
+ });
74
+ // Undefined unless the deployment configured one — `minimal` has no
75
+ // reranker by definition, which is what keeps it runnable without a GPU.
76
+ const reranker = rerankerFor(config);
77
+ const idempotency = new Idempotency({
78
+ redis,
79
+ onDegraded: (error) => {
80
+ logger.warn('idempotency cache unavailable; request processed uncached', { error: String(error).slice(0, 200) });
81
+ },
82
+ });
83
+ const login = new Login({
84
+ pool,
85
+ key,
86
+ issuer: config.jwtIssuer,
87
+ audience: config.jwtAudience,
88
+ accessTokenTtl: config.accessTokenTtl,
89
+ refreshTokenTtl: config.refreshTokenTtl,
90
+ role: APP_ROLE,
91
+ });
92
+ // What this process cannot serve a request without.
93
+ //
94
+ // S3 is checked now, and only when a deployment configured it: ingest writes
95
+ // document bytes there before it writes the row, so a bucket that is
96
+ // unreachable or a credential that is wrong is a surface that accepts nothing.
97
+ // A deployment without object storage reports no s3 key at all rather than a
98
+ // `true` that means "not asked" — those must not look the same.
99
+ //
100
+ // The embedder is still not checked: it is an external endpoint an operator
101
+ // supplies, a search fails loudly without it, and making readiness depend on
102
+ // somebody else's uptime turns their outage into a rollout that never
103
+ // completes.
104
+ const ready = async () => {
105
+ const [postgres, qdrant, redisUp, s3Up] = await Promise.all([
106
+ withOrg(pool, '00000000-0000-0000-0000-000000000000', async (c) => {
107
+ await c.query('SELECT 1');
108
+ return true;
109
+ }, { role: APP_ROLE }).catch(() => false),
110
+ vectors.ready().catch(() => false),
111
+ redis.ping().catch(() => false),
112
+ objects === undefined ? Promise.resolve(undefined) : objects.ready().catch(() => false),
113
+ ]);
114
+ return {
115
+ postgres,
116
+ qdrant,
117
+ redis: redisUp,
118
+ ...(s3Up === undefined ? {} : { s3: s3Up }),
119
+ };
120
+ };
57
121
  const server = createApi({
58
122
  verify: {
59
123
  key,
124
+ // Empty outside a rotation. During one it carries the key being retired,
125
+ // so tokens already in the wild keep verifying until they expire.
126
+ ...(alsoAccept.length === 0 ? {} : { alsoAccept }),
60
127
  issuer: config.jwtIssuer,
61
128
  audience: config.jwtAudience,
62
129
  serviceKeys: new PostgresServiceKeys(pool, APP_ROLE),
63
130
  },
64
131
  metrics: registry,
65
- documents: new PostgresDocuments(pool, APP_ROLE),
132
+ resourceMetadata: protectedResourceMetadata({
133
+ canonicalUrl: config.canonicalUrl,
134
+ ...(config.oauthAuthorizationServer === ''
135
+ ? {}
136
+ : { authorizationServer: config.oauthAuthorizationServer }),
137
+ }),
138
+ // The request path writes what it measures. Four of these were registered
139
+ // and never written, so /metrics served zeros forever — which reads as
140
+ // health rather than as absence.
141
+ observe: metrics,
142
+ ready,
143
+ maxBodyBytes: config.maxDocumentBytes,
144
+ limits,
145
+ limitPolicies,
146
+ trustProxy: config.trustProxy,
147
+ ...(config.metricsToken === undefined ? {} : { metricsToken: config.metricsToken }),
148
+ idempotency,
149
+ login,
150
+ auditReader: new PostgresAuditReader(pool, APP_ROLE, principalsCache),
151
+ reindex: new PostgresReindex(pool, vectors, APP_ROLE, principalsCache),
152
+ documents: new PostgresDocuments(pool, vectors, APP_ROLE, principalsCache, objects === undefined
153
+ ? undefined
154
+ : { url: (key) => objects.presign(key, config.presignTtl) }),
66
155
  search: new NacreSearchService({
156
+ principalsCache,
67
157
  pool,
68
158
  vectors,
69
- embedder,
70
- orgSlug,
71
- vectorName: vectorName(config.embeddingModel, config.embeddingDim),
159
+ embedderFor: HttpEmbedder.pool(),
160
+ role: APP_ROLE,
161
+ ...(reranker === undefined ? {} : { reranker }),
162
+ rerankCandidates: config.rerankCandidates,
163
+ onRerankFailed: (error) => {
164
+ // Answered in fusion order rather than not at all. Reranking decides
165
+ // ordering over candidates the index already filtered by permission,
166
+ // so this is a quality degradation and not a permissions one — and an
167
+ // operator who turned it on is entitled to know it is not running.
168
+ logger.warn('reranking failed; results are in fusion order', { error: String(error).slice(0, 200) });
169
+ },
170
+ }),
171
+ ingest: new NacreIngest({
172
+ pool,
173
+ principalsCache,
174
+ tombstone: vectors,
175
+ ...(objects === undefined ? {} : { objects }),
72
176
  role: APP_ROLE,
73
177
  }),
74
- ingest: new NacreIngest({ pool, tombstone: vectors, orgSlug, role: APP_ROLE }),
75
178
  audit: new PostgresAudit(pool, APP_ROLE),
76
- jobs: new PostgresJobs(pool, APP_ROLE),
77
- layers: new PostgresLayers(pool, APP_ROLE),
78
- grants: new PostgresGrants(pool, APP_ROLE),
179
+ auditQueryText: config.auditQueryText,
180
+ jobs: new PostgresJobs(pool, APP_ROLE, principalsCache),
181
+ layers: new PostgresLayers(pool, vectors, APP_ROLE, principalsCache),
182
+ workspaces: new PostgresWorkspaces(pool, APP_ROLE, principalsCache),
183
+ grants: new PostgresGrants(pool, APP_ROLE, principalsCache),
79
184
  serviceAccounts: new PostgresServiceAccounts(pool, APP_ROLE),
80
185
  });
81
186
  const port = Number(process.env.PORT ?? 8080);
82
187
  server.listen(port, () => {
83
188
  // The fingerprint, never the secret. An operator needs to know which key is
84
189
  // in use when two environments disagree; nobody needs the key in a log.
85
- const fingerprint = createHash('sha256').update(key).digest('hex').slice(0, 12);
86
- console.log(JSON.stringify({
87
- msg: 'api listening',
88
- port,
190
+ const print = (k) => `sha256:${createHash('sha256').update(k).digest('hex').slice(0, 12)}`;
191
+ logger.info('api listening', { port,
89
192
  env: config.env,
90
193
  issuer: config.jwtIssuer,
91
- jwt_key: `sha256:${fingerprint}`,
92
- }));
194
+ jwt_key: print(key),
195
+ // Present only during a rotation, which is exactly when an operator is
196
+ // reading this line. Its absence is how they know the rotation is
197
+ // finished and the old key is out.
198
+ ...(alsoAccept.length === 0 ? {} : { jwt_key_previous: alsoAccept.map(print) }) });
93
199
  });
94
- const shutdown = (signal) => {
95
- console.log(JSON.stringify({ msg: 'shutting down', signal }));
200
+ onListenError(server, 'api', port);
201
+ installGuards({
202
+ service: 'api',
96
203
  // Stop accepting, let in-flight requests finish, then release the pool.
97
- // Dropping a request mid-flight would leave its audit event unwritten.
98
- server.close(() => {
99
- void pool.end().then(() => process.exit(0));
100
- });
101
- };
102
- process.on('SIGTERM', () => shutdown('SIGTERM'));
103
- process.on('SIGINT', () => shutdown('SIGINT'));
204
+ // Dropping a request mid-flight would leave its audit event unwritten —
205
+ // but `server.close` waits without a bound, so `installGuards` puts one on
206
+ // it: one request stuck on a slow dependency used to mean the callback
207
+ // never ran, the pool was never released, and the orchestrator SIGKILLed at
208
+ // the end of its grace period. Every rolling deploy was an abrupt one.
209
+ shutdown: async () => {
210
+ await new Promise((resolve) => server.close(() => resolve()));
211
+ redis.close();
212
+ await pool.end();
213
+ },
214
+ });
104
215
  }
105
216
  main().catch((error) => {
106
217
  if (error instanceof ConfigError) {
107
218
  console.error(error.message);
108
219
  process.exit(2);
109
220
  }
110
- console.error(JSON.stringify({ msg: 'failed to start', error: String(error) }));
221
+ logger.error('failed to start', { error: String(error) });
111
222
  process.exit(1);
112
223
  });
113
224
  //# sourceMappingURL=main.js.map
package/dist/main.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}