@tumbaland/backend-core 1.29.0 → 1.31.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 (38) hide show
  1. package/dist/apiKeys/ApiKey.d.ts +51 -0
  2. package/dist/apiKeys/ApiKey.d.ts.map +1 -0
  3. package/dist/apiKeys/ApiKey.js +63 -0
  4. package/dist/apiKeys/ApiKey.js.map +1 -0
  5. package/dist/apiKeys/crypto.d.ts +61 -0
  6. package/dist/apiKeys/crypto.d.ts.map +1 -0
  7. package/dist/apiKeys/crypto.js +132 -0
  8. package/dist/apiKeys/crypto.js.map +1 -0
  9. package/dist/apiKeys/index.d.ts +10 -0
  10. package/dist/apiKeys/index.d.ts.map +1 -0
  11. package/dist/apiKeys/index.js +25 -0
  12. package/dist/apiKeys/index.js.map +1 -0
  13. package/dist/apiKeys/middleware.d.ts +54 -0
  14. package/dist/apiKeys/middleware.d.ts.map +1 -0
  15. package/dist/apiKeys/middleware.js +174 -0
  16. package/dist/apiKeys/middleware.js.map +1 -0
  17. package/dist/apiKeys/service.d.ts +43 -0
  18. package/dist/apiKeys/service.d.ts.map +1 -0
  19. package/dist/apiKeys/service.js +122 -0
  20. package/dist/apiKeys/service.js.map +1 -0
  21. package/dist/apiKeys/types.d.ts +50 -0
  22. package/dist/apiKeys/types.d.ts.map +1 -0
  23. package/dist/apiKeys/types.js +24 -0
  24. package/dist/apiKeys/types.js.map +1 -0
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/apiKeys/ApiKey.ts +75 -0
  31. package/src/apiKeys/crypto.test.ts +161 -0
  32. package/src/apiKeys/crypto.ts +163 -0
  33. package/src/apiKeys/index.ts +21 -0
  34. package/src/apiKeys/middleware.test.ts +254 -0
  35. package/src/apiKeys/middleware.ts +206 -0
  36. package/src/apiKeys/service.ts +149 -0
  37. package/src/apiKeys/types.ts +69 -0
  38. package/src/index.ts +3 -0
@@ -0,0 +1,51 @@
1
+ import mongoose, { Document } from 'mongoose';
2
+ import type { ApiKeyScope } from './types';
3
+ /**
4
+ * One API key. Lives in backend-core rather than auth-service because both
5
+ * halves need it: auth-service issues and revokes keys, and every other service
6
+ * verifies them on the way in. All services share one database, so the model is
7
+ * shared rather than the lookup going over HTTP on every request.
8
+ */
9
+ export interface IApiKey extends Document {
10
+ userId: string;
11
+ /**
12
+ * The owner's identity as it stood when the key was issued.
13
+ *
14
+ * Snapshotted so verifying a key stays a single indexed read. Every
15
+ * authenticated request would otherwise need a second query into the users
16
+ * collection to fill in a `UserPayload`, to serve fields almost no handler
17
+ * reads. The cost is that a later rename shows the old name on requests made
18
+ * with this key, which is a fair price and easy to reason about.
19
+ */
20
+ userEmail: string;
21
+ userName: string;
22
+ /** what the user called it — "Claude Code", "home assistant" */
23
+ name: string;
24
+ /** the public half of the token; unique, and what a presented key is looked up by */
25
+ keyId: string;
26
+ /** SHA-256 of the secret half — what verification actually compares against */
27
+ hash: string;
28
+ /** the whole token, encrypted, so the owner can read it back later */
29
+ sealedCiphertext: string;
30
+ sealedIv: string;
31
+ sealedTag: string;
32
+ scopes: ApiKeyScope[];
33
+ /** the tenant this key acts in; null means the user's own non-group data */
34
+ groupId: string | null;
35
+ lastUsedAt?: Date;
36
+ expiresAt?: Date;
37
+ revokedAt?: Date;
38
+ /** reveals are counted and timestamped — reading back a key is worth an audit trail */
39
+ revealCount: number;
40
+ lastRevealedAt?: Date;
41
+ createdAt: Date;
42
+ updatedAt: Date;
43
+ }
44
+ export declare const ApiKey: mongoose.Model<IApiKey, {}, {}, {}, mongoose.Document<unknown, {}, IApiKey, {}, mongoose.DefaultSchemaOptions> & IApiKey & Required<{
45
+ _id: mongoose.Types.ObjectId;
46
+ }> & {
47
+ __v: number;
48
+ } & {
49
+ id: string;
50
+ }, any, IApiKey>;
51
+ //# sourceMappingURL=ApiKey.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ApiKey.d.ts","sourceRoot":"","sources":["../../src/apiKeys/ApiKey.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,EAAE,EAAE,QAAQ,EAAU,MAAM,UAAU,CAAC;AACtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,WAAW,OAAQ,SAAQ,QAAQ;IACvC,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;OAQG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,4EAA4E;IAC5E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,CAAC,EAAE,IAAI,CAAC;IAClB,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,uFAAuF;IACvF,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,IAAI,CAAC;IACtB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AA6BD,eAAO,MAAM,MAAM;;;;;;gBAEgC,CAAC"}
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ApiKey = void 0;
37
+ const mongoose_1 = __importStar(require("mongoose"));
38
+ const ApiKeySchema = new mongoose_1.Schema({
39
+ userId: { type: String, required: true, index: true },
40
+ userEmail: { type: String, required: true },
41
+ userName: { type: String, required: true, default: '' },
42
+ name: { type: String, required: true, trim: true, maxlength: 60 },
43
+ keyId: { type: String, required: true, unique: true, index: true },
44
+ hash: { type: String, required: true },
45
+ sealedCiphertext: { type: String, required: true },
46
+ sealedIv: { type: String, required: true },
47
+ sealedTag: { type: String, required: true },
48
+ scopes: { type: [String], default: [] },
49
+ // Explicitly nullable rather than optional: "personal scope" is a decision the
50
+ // creator made, and it must not be indistinguishable from a field nobody set.
51
+ groupId: { type: String, default: null },
52
+ lastUsedAt: { type: Date },
53
+ expiresAt: { type: Date },
54
+ revokedAt: { type: Date },
55
+ revealCount: { type: Number, default: 0 },
56
+ lastRevealedAt: { type: Date }
57
+ }, { timestamps: true, collection: 'api_keys' });
58
+ // Every listing is one user's keys, newest first.
59
+ ApiKeySchema.index({ userId: 1, createdAt: -1 });
60
+ exports.ApiKey = mongoose_1.default.models.ApiKey
61
+ ? mongoose_1.default.models.ApiKey
62
+ : mongoose_1.default.model('ApiKey', ApiKeySchema);
63
+ //# sourceMappingURL=ApiKey.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ApiKey.js","sourceRoot":"","sources":["../../src/apiKeys/ApiKey.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qDAAsD;AA6CtD,MAAM,YAAY,GAAG,IAAI,iBAAM,CAC7B;IACE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IACrD,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC3C,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE;IACvD,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE;IACjE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IAClE,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACtC,gBAAgB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAClD,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC3C,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE;IACvC,+EAA+E;IAC/E,8EAA8E;IAC9E,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE;IACxC,UAAU,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE;IAC1B,SAAS,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE;IACzB,SAAS,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE;IACzB,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE;IACzC,cAAc,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE;CAC/B,EACD,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,CAC7C,CAAC;AAEF,kDAAkD;AAClD,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;AAEpC,QAAA,MAAM,GAAG,kBAAQ,CAAC,MAAM,CAAC,MAAM;IAC1C,CAAC,CAAE,kBAAQ,CAAC,MAAM,CAAC,MAAkC;IACrD,CAAC,CAAC,kBAAQ,CAAC,KAAK,CAAU,QAAQ,EAAE,YAAY,CAAC,CAAC"}
@@ -0,0 +1,61 @@
1
+ export interface GeneratedKey {
2
+ /** the whole key, shown to the user and never stored in this form */
3
+ token: string;
4
+ /** the public half, indexed for lookup */
5
+ id: string;
6
+ /** SHA-256 of the secret half, for verification */
7
+ hash: string;
8
+ }
9
+ /** Test seam: the derived key is cached for the life of the process. */
10
+ export declare const resetEncryptionKeyCache: () => void;
11
+ /**
12
+ * Whether keys can be issued and revealed at all.
13
+ *
14
+ * Only the reversible copy needs the secret — verification runs off the digest —
15
+ * so a service that merely accepts keys works without it. That asymmetry is easy
16
+ * to get wrong in a deployment, so callers can ask rather than discovering it
17
+ * through a 500 on someone's first key.
18
+ */
19
+ export declare const isEncryptionConfigured: () => boolean;
20
+ export declare const sha256: (value: string) => string;
21
+ export declare const generateKey: () => GeneratedKey;
22
+ export interface ParsedKey {
23
+ id: string;
24
+ secret: string;
25
+ }
26
+ /**
27
+ * Split a presented key into its public id and its secret half.
28
+ *
29
+ * Returns null rather than throwing: an unparseable token is an ordinary failed
30
+ * authentication, not an exceptional condition, and the caller answers both the
31
+ * same way.
32
+ */
33
+ export declare const parseKey: (token: unknown) => ParsedKey | null;
34
+ /** True when a token even looks like one of ours — lets the caller skip a JWT parse. */
35
+ export declare const looksLikeApiKey: (token: unknown) => boolean;
36
+ /** Constant-time digest comparison, so a wrong key leaks nothing through timing. */
37
+ export declare const secretMatches: (secret: string, expectedHash: string) => boolean;
38
+ export interface SealedSecret {
39
+ ciphertext: string;
40
+ iv: string;
41
+ tag: string;
42
+ }
43
+ /**
44
+ * Encrypt the key so it can be shown again later.
45
+ *
46
+ * Storing a recoverable copy is weaker than hashing alone: whoever holds both
47
+ * the database and `API_KEY_ENCRYPTION_SECRET` can mint the plaintext. It buys
48
+ * the ability to re-read a key you have lost, which the product wants. The
49
+ * secret lives outside the database precisely so that a dump, a backup, or a
50
+ * read-only replica is not on its own enough.
51
+ */
52
+ export declare const encryptSecret: (token: string) => SealedSecret;
53
+ /**
54
+ * Recover a stored key. Throws when the ciphertext has been tampered with —
55
+ * GCM authenticates, so a modified record fails loudly instead of returning
56
+ * plausible rubbish.
57
+ */
58
+ export declare const decryptSecret: (sealed: SealedSecret) => string;
59
+ /** What the UI shows in a list: enough to tell two keys apart, not enough to use one. */
60
+ export declare const displayPrefix: (id: string) => string;
61
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/apiKeys/crypto.ts"],"names":[],"mappings":"AAiCA,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,EAAE,EAAE,MAAM,CAAC;IACX,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;CACd;AAWD,wEAAwE;AACxE,eAAO,MAAM,uBAAuB,QAAO,IAE1C,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,QAAO,OACM,CAAC;AAEjD,eAAO,MAAM,MAAM,GAAI,OAAO,MAAM,KAAG,MACmB,CAAC;AAE3D,eAAO,MAAM,WAAW,QAAO,YAS9B,CAAC;AAEF,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,GAAI,OAAO,OAAO,KAAG,SAAS,GAAG,IAYrD,CAAC;AAEF,wFAAwF;AACxF,eAAO,MAAM,eAAe,GAAI,OAAO,OAAO,KAAG,OACgB,CAAC;AAElE,oFAAoF;AACpF,eAAO,MAAM,aAAa,GAAI,QAAQ,MAAM,EAAE,cAAc,MAAM,KAAG,OAKpE,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,YAU7C,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,QAAQ,YAAY,KAAG,MAQpD,CAAC;AAEF,yFAAyF;AACzF,eAAO,MAAM,aAAa,GAAI,IAAI,MAAM,KAAG,MAA+B,CAAC"}
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.displayPrefix = exports.decryptSecret = exports.encryptSecret = exports.secretMatches = exports.looksLikeApiKey = exports.parseKey = exports.generateKey = exports.sha256 = exports.isEncryptionConfigured = exports.resetEncryptionKeyCache = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const env_1 = require("../config/env");
6
+ /**
7
+ * Key material and the two different things we do with it.
8
+ *
9
+ * A presented key is checked against a SHA-256 digest — fast, because it runs on
10
+ * every authenticated request. Plain SHA-256 rather than bcrypt is right here
11
+ * and wrong for passwords: the secret is 32 bytes from a CSPRNG, so there is no
12
+ * dictionary to attack and nothing for a slow hash to buy.
13
+ *
14
+ * Separately, the key is stored encrypted so the owner can look it up again
15
+ * later. That is a deliberate trade — see `encryptSecret` — and the reason the
16
+ * digest is kept as well: verification never touches the reversible copy.
17
+ */
18
+ /** `tmb_live_<id>_<secret>` — the prefix makes a leaked key greppable in logs. */
19
+ const KEY_PREFIX = 'tmb_live';
20
+ const ID_BYTES = 6; // 12 hex chars, the public half
21
+ const SECRET_BYTES = 32;
22
+ const ALGORITHM = 'aes-256-gcm';
23
+ const IV_BYTES = 12;
24
+ /** Fixed: the input is a high-entropy env secret, not a password, so a per-record salt buys nothing. */
25
+ const KDF_SALT = 'tumbaland/api-key/v1';
26
+ /** Derive the AES key once per process; scrypt is deliberately slow. */
27
+ let cachedKey = null;
28
+ function encryptionKey() {
29
+ if (!cachedKey) {
30
+ cachedKey = (0, crypto_1.scryptSync)((0, env_1.requireEnv)('API_KEY_ENCRYPTION_SECRET'), KDF_SALT, 32);
31
+ }
32
+ return cachedKey;
33
+ }
34
+ /** Test seam: the derived key is cached for the life of the process. */
35
+ const resetEncryptionKeyCache = () => {
36
+ cachedKey = null;
37
+ };
38
+ exports.resetEncryptionKeyCache = resetEncryptionKeyCache;
39
+ /**
40
+ * Whether keys can be issued and revealed at all.
41
+ *
42
+ * Only the reversible copy needs the secret — verification runs off the digest —
43
+ * so a service that merely accepts keys works without it. That asymmetry is easy
44
+ * to get wrong in a deployment, so callers can ask rather than discovering it
45
+ * through a 500 on someone's first key.
46
+ */
47
+ const isEncryptionConfigured = () => Boolean(process.env.API_KEY_ENCRYPTION_SECRET);
48
+ exports.isEncryptionConfigured = isEncryptionConfigured;
49
+ const sha256 = (value) => (0, crypto_1.createHash)('sha256').update(value, 'utf8').digest('hex');
50
+ exports.sha256 = sha256;
51
+ const generateKey = () => {
52
+ const id = (0, crypto_1.randomBytes)(ID_BYTES).toString('hex');
53
+ const secret = (0, crypto_1.randomBytes)(SECRET_BYTES).toString('base64url');
54
+ return {
55
+ token: `${KEY_PREFIX}_${id}_${secret}`,
56
+ id,
57
+ hash: (0, exports.sha256)(secret)
58
+ };
59
+ };
60
+ exports.generateKey = generateKey;
61
+ /**
62
+ * Split a presented key into its public id and its secret half.
63
+ *
64
+ * Returns null rather than throwing: an unparseable token is an ordinary failed
65
+ * authentication, not an exceptional condition, and the caller answers both the
66
+ * same way.
67
+ */
68
+ const parseKey = (token) => {
69
+ if (typeof token !== 'string')
70
+ return null;
71
+ // Matched positionally rather than split on '_': the base64url alphabet
72
+ // includes '_', so a secret can contain any number of them and splitting
73
+ // would reject roughly a third of otherwise valid keys. The id is
74
+ // fixed-width, so everything after it is the secret.
75
+ const match = token.match(/^tmb_live_([0-9a-f]{12})_(.+)$/);
76
+ if (!match)
77
+ return null;
78
+ if (match[2].length < 16)
79
+ return null;
80
+ return { id: match[1], secret: match[2] };
81
+ };
82
+ exports.parseKey = parseKey;
83
+ /** True when a token even looks like one of ours — lets the caller skip a JWT parse. */
84
+ const looksLikeApiKey = (token) => typeof token === 'string' && token.startsWith(`${KEY_PREFIX}_`);
85
+ exports.looksLikeApiKey = looksLikeApiKey;
86
+ /** Constant-time digest comparison, so a wrong key leaks nothing through timing. */
87
+ const secretMatches = (secret, expectedHash) => {
88
+ const presented = Buffer.from((0, exports.sha256)(secret), 'hex');
89
+ const expected = Buffer.from(expectedHash, 'hex');
90
+ if (presented.length !== expected.length)
91
+ return false;
92
+ return (0, crypto_1.timingSafeEqual)(presented, expected);
93
+ };
94
+ exports.secretMatches = secretMatches;
95
+ /**
96
+ * Encrypt the key so it can be shown again later.
97
+ *
98
+ * Storing a recoverable copy is weaker than hashing alone: whoever holds both
99
+ * the database and `API_KEY_ENCRYPTION_SECRET` can mint the plaintext. It buys
100
+ * the ability to re-read a key you have lost, which the product wants. The
101
+ * secret lives outside the database precisely so that a dump, a backup, or a
102
+ * read-only replica is not on its own enough.
103
+ */
104
+ const encryptSecret = (token) => {
105
+ const iv = (0, crypto_1.randomBytes)(IV_BYTES);
106
+ const cipher = (0, crypto_1.createCipheriv)(ALGORITHM, encryptionKey(), iv);
107
+ const ciphertext = Buffer.concat([cipher.update(token, 'utf8'), cipher.final()]);
108
+ return {
109
+ ciphertext: ciphertext.toString('base64'),
110
+ iv: iv.toString('base64'),
111
+ tag: cipher.getAuthTag().toString('base64')
112
+ };
113
+ };
114
+ exports.encryptSecret = encryptSecret;
115
+ /**
116
+ * Recover a stored key. Throws when the ciphertext has been tampered with —
117
+ * GCM authenticates, so a modified record fails loudly instead of returning
118
+ * plausible rubbish.
119
+ */
120
+ const decryptSecret = (sealed) => {
121
+ const decipher = (0, crypto_1.createDecipheriv)(ALGORITHM, encryptionKey(), Buffer.from(sealed.iv, 'base64'));
122
+ decipher.setAuthTag(Buffer.from(sealed.tag, 'base64'));
123
+ return Buffer.concat([
124
+ decipher.update(Buffer.from(sealed.ciphertext, 'base64')),
125
+ decipher.final()
126
+ ]).toString('utf8');
127
+ };
128
+ exports.decryptSecret = decryptSecret;
129
+ /** What the UI shows in a list: enough to tell two keys apart, not enough to use one. */
130
+ const displayPrefix = (id) => `${KEY_PREFIX}_${id}`;
131
+ exports.displayPrefix = displayPrefix;
132
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/apiKeys/crypto.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export { ApiKey } from './ApiKey';
2
+ export type { IApiKey } from './ApiKey';
3
+ export { createApiKey, listApiKeys, revealApiKey, revokeApiKey, deleteApiKey, verifyApiKey } from './service';
4
+ export type { CreateApiKeyInput, CreatedApiKey } from './service';
5
+ export { authenticateAgent, requireScope, denyApiKeys } from './middleware';
6
+ export type { ApiKeyContext } from './middleware';
7
+ export { API_KEY_SCOPES, isApiKeyScope } from './types';
8
+ export type { ApiKeyScope, ApiKeySummary, ApiKeyVerification, ApiKeyRejection } from './types';
9
+ export { looksLikeApiKey, displayPrefix, isEncryptionConfigured, resetEncryptionKeyCache } from './crypto';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/apiKeys/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EACL,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,YAAY,EACb,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC5E,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxD,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/F,OAAO,EACL,eAAe,EACf,aAAa,EACb,sBAAsB,EACtB,uBAAuB,EACxB,MAAM,UAAU,CAAC"}
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resetEncryptionKeyCache = exports.isEncryptionConfigured = exports.displayPrefix = exports.looksLikeApiKey = exports.isApiKeyScope = exports.API_KEY_SCOPES = exports.denyApiKeys = exports.requireScope = exports.authenticateAgent = exports.verifyApiKey = exports.deleteApiKey = exports.revokeApiKey = exports.revealApiKey = exports.listApiKeys = exports.createApiKey = exports.ApiKey = void 0;
4
+ var ApiKey_1 = require("./ApiKey");
5
+ Object.defineProperty(exports, "ApiKey", { enumerable: true, get: function () { return ApiKey_1.ApiKey; } });
6
+ var service_1 = require("./service");
7
+ Object.defineProperty(exports, "createApiKey", { enumerable: true, get: function () { return service_1.createApiKey; } });
8
+ Object.defineProperty(exports, "listApiKeys", { enumerable: true, get: function () { return service_1.listApiKeys; } });
9
+ Object.defineProperty(exports, "revealApiKey", { enumerable: true, get: function () { return service_1.revealApiKey; } });
10
+ Object.defineProperty(exports, "revokeApiKey", { enumerable: true, get: function () { return service_1.revokeApiKey; } });
11
+ Object.defineProperty(exports, "deleteApiKey", { enumerable: true, get: function () { return service_1.deleteApiKey; } });
12
+ Object.defineProperty(exports, "verifyApiKey", { enumerable: true, get: function () { return service_1.verifyApiKey; } });
13
+ var middleware_1 = require("./middleware");
14
+ Object.defineProperty(exports, "authenticateAgent", { enumerable: true, get: function () { return middleware_1.authenticateAgent; } });
15
+ Object.defineProperty(exports, "requireScope", { enumerable: true, get: function () { return middleware_1.requireScope; } });
16
+ Object.defineProperty(exports, "denyApiKeys", { enumerable: true, get: function () { return middleware_1.denyApiKeys; } });
17
+ var types_1 = require("./types");
18
+ Object.defineProperty(exports, "API_KEY_SCOPES", { enumerable: true, get: function () { return types_1.API_KEY_SCOPES; } });
19
+ Object.defineProperty(exports, "isApiKeyScope", { enumerable: true, get: function () { return types_1.isApiKeyScope; } });
20
+ var crypto_1 = require("./crypto");
21
+ Object.defineProperty(exports, "looksLikeApiKey", { enumerable: true, get: function () { return crypto_1.looksLikeApiKey; } });
22
+ Object.defineProperty(exports, "displayPrefix", { enumerable: true, get: function () { return crypto_1.displayPrefix; } });
23
+ Object.defineProperty(exports, "isEncryptionConfigured", { enumerable: true, get: function () { return crypto_1.isEncryptionConfigured; } });
24
+ Object.defineProperty(exports, "resetEncryptionKeyCache", { enumerable: true, get: function () { return crypto_1.resetEncryptionKeyCache; } });
25
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/apiKeys/index.ts"],"names":[],"mappings":";;;AAAA,mCAAkC;AAAzB,gGAAA,MAAM,OAAA;AAEf,qCAOmB;AANjB,uGAAA,YAAY,OAAA;AACZ,sGAAA,WAAW,OAAA;AACX,uGAAA,YAAY,OAAA;AACZ,uGAAA,YAAY,OAAA;AACZ,uGAAA,YAAY,OAAA;AACZ,uGAAA,YAAY,OAAA;AAGd,2CAA4E;AAAnE,+GAAA,iBAAiB,OAAA;AAAE,0GAAA,YAAY,OAAA;AAAE,yGAAA,WAAW,OAAA;AAErD,iCAAwD;AAA/C,uGAAA,cAAc,OAAA;AAAE,sGAAA,aAAa,OAAA;AAEtC,mCAKkB;AAJhB,yGAAA,eAAe,OAAA;AACf,uGAAA,aAAa,OAAA;AACb,gHAAA,sBAAsB,OAAA;AACtB,iHAAA,uBAAuB,OAAA"}
@@ -0,0 +1,54 @@
1
+ import { RequestHandler } from 'express';
2
+ import type { ApiKeyScope } from './types';
3
+ /**
4
+ * Request-scoped facts about the key a request arrived on. Absent on ordinary
5
+ * session requests, which is itself the signal a handler needs: `req.apiKey`
6
+ * being set means software is acting, not a person.
7
+ */
8
+ export interface ApiKeyContext {
9
+ keyId: string;
10
+ scopes: ApiKeyScope[];
11
+ /** the tenant this key is pinned to; null means the owner's personal data */
12
+ groupId: string | null;
13
+ }
14
+ declare global {
15
+ namespace Express {
16
+ interface Request {
17
+ apiKey?: ApiKeyContext;
18
+ }
19
+ }
20
+ }
21
+ /**
22
+ * Authenticate a request that software may legitimately be making.
23
+ *
24
+ * Accepts either an ordinary session — a person at a keyboard, who keeps the
25
+ * full run of their own account — or an API key carrying every one of
26
+ * `requiredScopes`.
27
+ *
28
+ * This is deliberately *not* what `authenticateToken` became. Keys stay refused
29
+ * everywhere by default, and a route opts in by naming the scopes it needs; the
30
+ * alternative, teaching the existing middleware about keys, would have silently
31
+ * opened every route in every service at once, account deletion included.
32
+ */
33
+ export declare const authenticateAgent: (...requiredScopes: ApiKeyScope[]) => RequestHandler;
34
+ /**
35
+ * Require a scope on a route already behind `authenticateAgent`.
36
+ *
37
+ * Splitting authentication from authorization lets one `router.use` cover a
38
+ * whole router while each route still states what it needs — so a read-only key
39
+ * reaches the GETs and stops at the POSTs, instead of being turned away at the
40
+ * door for lacking a scope half the router never uses.
41
+ *
42
+ * A session passes unconditionally: scopes narrow what software may do on a
43
+ * person's behalf, not what the person may do themselves.
44
+ */
45
+ export declare const requireScope: (...requiredScopes: ApiKeyScope[]) => RequestHandler;
46
+ /**
47
+ * Refuse API keys on a route that a session may still use.
48
+ *
49
+ * For the handful of operations that should stay a person's to perform — key
50
+ * management itself, most obviously, since a key that can mint keys is a key
51
+ * that cannot be revoked.
52
+ */
53
+ export declare const denyApiKeys: RequestHandler;
54
+ //# sourceMappingURL=middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/apiKeys/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,cAAc,EAAY,MAAM,SAAS,CAAC;AAO1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,MAAM,CAAC,EAAE,aAAa,CAAC;SACxB;KACF;CACF;AA0DD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB,GAAI,GAAG,gBAAgB,WAAW,EAAE,KAAG,cA4DlE,CAAC;AAEJ;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,GAAI,GAAG,gBAAgB,WAAW,EAAE,KAAG,cAiB7D,CAAC;AAEJ;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,cASzB,CAAC"}
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.denyApiKeys = exports.requireScope = exports.authenticateAgent = void 0;
7
+ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
8
+ const env_1 = require("../config/env");
9
+ const logger_1 = __importDefault(require("../logging/logger"));
10
+ const crypto_1 = require("./crypto");
11
+ const service_1 = require("./service");
12
+ /** Both auth paths read the token from the same two places. */
13
+ const extractToken = (req) => req.cookies?.access_token || req.headers.authorization?.replace('Bearer ', '');
14
+ const unauthorized = (res) => {
15
+ res.status(401).json({ success: false, message: 'Invalid or expired credentials' });
16
+ };
17
+ /**
18
+ * The tenant a request is asking to act in, wherever it named one.
19
+ *
20
+ * Handlers read `groupId` from either the query string or the body depending on
21
+ * the verb, so both are checked — a pin that only covered one of them would be
22
+ * no pin at all.
23
+ */
24
+ const requestedGroupId = (req) => {
25
+ const fromQuery = req.query?.groupId;
26
+ if (typeof fromQuery === 'string' && fromQuery.length > 0)
27
+ return fromQuery;
28
+ const fromBody = req.body?.groupId;
29
+ if (typeof fromBody === 'string' && fromBody.length > 0)
30
+ return fromBody;
31
+ return undefined;
32
+ };
33
+ /**
34
+ * Hold a key to the tenant it was issued for.
35
+ *
36
+ * Two things happen here, and the second is the one that makes keys pleasant to
37
+ * use. A request that names a *different* tenant is refused outright — the pin
38
+ * is the whole reason a key is safe to hand to an agent. A request that names
39
+ * none has the pinned tenant written in for it, so the agent never has to know a
40
+ * group id exists, and a handler's `if (groupId) ... else personal` branch lands
41
+ * where the key's owner intended.
42
+ */
43
+ const applyTenantPin = (req, groupId) => {
44
+ const requested = requestedGroupId(req);
45
+ if (requested !== undefined && requested !== groupId)
46
+ return false;
47
+ if (groupId !== null && requested === undefined) {
48
+ // Express 5 makes req.query a getter, so it is redefined rather than assigned.
49
+ Object.defineProperty(req, 'query', {
50
+ value: { ...req.query, groupId },
51
+ writable: true,
52
+ configurable: true,
53
+ enumerable: true
54
+ });
55
+ if (req.body && typeof req.body === 'object') {
56
+ req.body.groupId = groupId;
57
+ }
58
+ }
59
+ return true;
60
+ };
61
+ /**
62
+ * Authenticate a request that software may legitimately be making.
63
+ *
64
+ * Accepts either an ordinary session — a person at a keyboard, who keeps the
65
+ * full run of their own account — or an API key carrying every one of
66
+ * `requiredScopes`.
67
+ *
68
+ * This is deliberately *not* what `authenticateToken` became. Keys stay refused
69
+ * everywhere by default, and a route opts in by naming the scopes it needs; the
70
+ * alternative, teaching the existing middleware about keys, would have silently
71
+ * opened every route in every service at once, account deletion included.
72
+ */
73
+ const authenticateAgent = (...requiredScopes) => async function authenticateAgentHandler(req, res, next) {
74
+ const token = extractToken(req);
75
+ if (!token) {
76
+ res.status(401).json({ success: false, message: 'Access token required' });
77
+ return;
78
+ }
79
+ if (!(0, crypto_1.looksLikeApiKey)(token)) {
80
+ try {
81
+ const user = jsonwebtoken_1.default.verify(token, (0, env_1.requireEnv)('JWT_SECRET'));
82
+ req.user = user;
83
+ req.userGroups = user.groups ?? [];
84
+ next();
85
+ }
86
+ catch {
87
+ unauthorized(res);
88
+ }
89
+ return;
90
+ }
91
+ const result = await (0, service_1.verifyApiKey)(token);
92
+ if (!result.ok) {
93
+ // The client is told only that the credentials failed; which of the five
94
+ // ways it failed is a detail that would help someone guessing.
95
+ logger_1.default.warn('API key rejected', { rejection: result.rejection, path: req.path });
96
+ unauthorized(res);
97
+ return;
98
+ }
99
+ const scopes = result.scopes ?? [];
100
+ const missing = requiredScopes.filter((scope) => !scopes.includes(scope));
101
+ if (missing.length > 0) {
102
+ res.status(403).json({
103
+ success: false,
104
+ message: `API key is missing required scope: ${missing.join(', ')}`
105
+ });
106
+ return;
107
+ }
108
+ const groupId = result.groupId ?? null;
109
+ if (!applyTenantPin(req, groupId)) {
110
+ res.status(403).json({
111
+ success: false,
112
+ message: 'API key is not permitted to act in the requested group'
113
+ });
114
+ return;
115
+ }
116
+ req.user = {
117
+ id: result.userId,
118
+ email: result.userEmail ?? '',
119
+ name: result.userName ?? ''
120
+ };
121
+ // Only the pinned group, never the owner's full membership: this is what
122
+ // stops a key reaching a group it was not issued for through any handler
123
+ // that consults `userGroups` instead of the `groupId` parameter.
124
+ req.userGroups = groupId ? [groupId] : [];
125
+ req.apiKey = { keyId: result.keyId, scopes, groupId };
126
+ next();
127
+ };
128
+ exports.authenticateAgent = authenticateAgent;
129
+ /**
130
+ * Require a scope on a route already behind `authenticateAgent`.
131
+ *
132
+ * Splitting authentication from authorization lets one `router.use` cover a
133
+ * whole router while each route still states what it needs — so a read-only key
134
+ * reaches the GETs and stops at the POSTs, instead of being turned away at the
135
+ * door for lacking a scope half the router never uses.
136
+ *
137
+ * A session passes unconditionally: scopes narrow what software may do on a
138
+ * person's behalf, not what the person may do themselves.
139
+ */
140
+ const requireScope = (...requiredScopes) => function requireScopeHandler(req, res, next) {
141
+ if (!req.apiKey) {
142
+ next();
143
+ return;
144
+ }
145
+ const missing = requiredScopes.filter((scope) => !req.apiKey.scopes.includes(scope));
146
+ if (missing.length > 0) {
147
+ res.status(403).json({
148
+ success: false,
149
+ message: `API key is missing required scope: ${missing.join(', ')}`
150
+ });
151
+ return;
152
+ }
153
+ next();
154
+ };
155
+ exports.requireScope = requireScope;
156
+ /**
157
+ * Refuse API keys on a route that a session may still use.
158
+ *
159
+ * For the handful of operations that should stay a person's to perform — key
160
+ * management itself, most obviously, since a key that can mint keys is a key
161
+ * that cannot be revoked.
162
+ */
163
+ const denyApiKeys = (req, res, next) => {
164
+ if (req.apiKey) {
165
+ res.status(403).json({
166
+ success: false,
167
+ message: 'This operation requires an interactive session, not an API key'
168
+ });
169
+ return;
170
+ }
171
+ next();
172
+ };
173
+ exports.denyApiKeys = denyApiKeys;
174
+ //# sourceMappingURL=middleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/apiKeys/middleware.ts"],"names":[],"mappings":"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"}