@tumbaland/backend-core 1.37.0 → 1.39.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 (92) hide show
  1. package/dist/apiKeys/ApiKey.d.ts +23 -3
  2. package/dist/apiKeys/ApiKey.d.ts.map +1 -1
  3. package/dist/apiKeys/ApiKey.js +19 -2
  4. package/dist/apiKeys/ApiKey.js.map +1 -1
  5. package/dist/apiKeys/index.d.ts +2 -2
  6. package/dist/apiKeys/index.d.ts.map +1 -1
  7. package/dist/apiKeys/index.js +5 -1
  8. package/dist/apiKeys/index.js.map +1 -1
  9. package/dist/apiKeys/middleware.d.ts +17 -3
  10. package/dist/apiKeys/middleware.d.ts.map +1 -1
  11. package/dist/apiKeys/middleware.js +71 -34
  12. package/dist/apiKeys/middleware.js.map +1 -1
  13. package/dist/apiKeys/service.d.ts +5 -3
  14. package/dist/apiKeys/service.d.ts.map +1 -1
  15. package/dist/apiKeys/service.js +11 -3
  16. package/dist/apiKeys/service.js.map +1 -1
  17. package/dist/apiKeys/types.d.ts +43 -8
  18. package/dist/apiKeys/types.d.ts.map +1 -1
  19. package/dist/apiKeys/types.js +35 -1
  20. package/dist/apiKeys/types.js.map +1 -1
  21. package/dist/audit/AuditEvent.d.ts +51 -0
  22. package/dist/audit/AuditEvent.d.ts.map +1 -0
  23. package/dist/audit/AuditEvent.js +69 -0
  24. package/dist/audit/AuditEvent.js.map +1 -0
  25. package/dist/audit/actor.d.ts +41 -0
  26. package/dist/audit/actor.d.ts.map +1 -0
  27. package/dist/audit/actor.js +39 -0
  28. package/dist/audit/actor.js.map +1 -0
  29. package/dist/audit/context.d.ts +40 -0
  30. package/dist/audit/context.d.ts.map +1 -0
  31. package/dist/audit/context.js +60 -0
  32. package/dist/audit/context.js.map +1 -0
  33. package/dist/audit/index.d.ts +12 -0
  34. package/dist/audit/index.d.ts.map +1 -0
  35. package/dist/audit/index.js +22 -0
  36. package/dist/audit/index.js.map +1 -0
  37. package/dist/audit/plugin.d.ts +23 -0
  38. package/dist/audit/plugin.d.ts.map +1 -0
  39. package/dist/audit/plugin.js +226 -0
  40. package/dist/audit/plugin.js.map +1 -0
  41. package/dist/audit/reads.d.ts +47 -0
  42. package/dist/audit/reads.d.ts.map +1 -0
  43. package/dist/audit/reads.js +94 -0
  44. package/dist/audit/reads.js.map +1 -0
  45. package/dist/audit/service.d.ts +49 -0
  46. package/dist/audit/service.d.ts.map +1 -0
  47. package/dist/audit/service.js +65 -0
  48. package/dist/audit/service.js.map +1 -0
  49. package/dist/index.d.ts +1 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1 -0
  52. package/dist/index.js.map +1 -1
  53. package/dist/oauth/models.d.ts +13 -2
  54. package/dist/oauth/models.d.ts.map +1 -1
  55. package/dist/oauth/models.js +19 -0
  56. package/dist/oauth/models.js.map +1 -1
  57. package/dist/oauth/service.d.ts +15 -8
  58. package/dist/oauth/service.d.ts.map +1 -1
  59. package/dist/oauth/service.js +10 -3
  60. package/dist/oauth/service.js.map +1 -1
  61. package/dist/oauth/tokens.d.ts +37 -5
  62. package/dist/oauth/tokens.d.ts.map +1 -1
  63. package/dist/oauth/tokens.js +9 -2
  64. package/dist/oauth/tokens.js.map +1 -1
  65. package/package.json +1 -1
  66. package/src/apiKeys/ApiKey.test.ts +25 -1
  67. package/src/apiKeys/ApiKey.ts +31 -4
  68. package/src/apiKeys/index.ts +16 -2
  69. package/src/apiKeys/middleware.test.ts +126 -20
  70. package/src/apiKeys/middleware.ts +99 -39
  71. package/src/apiKeys/service.test.ts +58 -9
  72. package/src/apiKeys/service.ts +25 -6
  73. package/src/apiKeys/types.ts +63 -8
  74. package/src/audit/AuditEvent.ts +79 -0
  75. package/src/audit/actor.test.ts +95 -0
  76. package/src/audit/actor.ts +68 -0
  77. package/src/audit/context.test.ts +91 -0
  78. package/src/audit/context.ts +83 -0
  79. package/src/audit/index.ts +11 -0
  80. package/src/audit/plugin.test.ts +258 -0
  81. package/src/audit/plugin.ts +254 -0
  82. package/src/audit/reads.test.ts +164 -0
  83. package/src/audit/reads.ts +88 -0
  84. package/src/audit/service.test.ts +115 -0
  85. package/src/audit/service.ts +92 -0
  86. package/src/index.ts +1 -0
  87. package/src/middleware/authMiddleware.test.ts +6 -1
  88. package/src/oauth/models.ts +32 -2
  89. package/src/oauth/service.test.ts +58 -6
  90. package/src/oauth/service.ts +25 -7
  91. package/src/oauth/tokens.test.ts +22 -5
  92. package/src/oauth/tokens.ts +52 -7
@@ -9,7 +9,7 @@
9
9
  * it writes to rather than choosing one per request the way the UI does.
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.isApiKeyScope = exports.API_KEY_SCOPES = void 0;
12
+ exports.readTenants = exports.groupIdsOf = exports.groupIdOf = exports.PERSONAL_TENANT = exports.isApiKeyScope = exports.API_KEY_SCOPES = void 0;
13
13
  /** Everything a key may be granted. Read and write are separate on purpose. */
14
14
  exports.API_KEY_SCOPES = [
15
15
  'relationship:read',
@@ -21,4 +21,38 @@ exports.API_KEY_SCOPES = [
21
21
  ];
22
22
  const isApiKeyScope = (value) => typeof value === 'string' && exports.API_KEY_SCOPES.includes(value);
23
23
  exports.isApiKeyScope = isApiKeyScope;
24
+ /**
25
+ * A tenant a credential may act in: a group id, or the user's own data.
26
+ *
27
+ * Personal is a named sentinel rather than `null` so that a tenant is always a
28
+ * plain string — storable in an array, sendable in a JWT claim, comparable
29
+ * without a special case at every layer. It cannot collide with a real tenant:
30
+ * group ids are 24-character hex.
31
+ */
32
+ exports.PERSONAL_TENANT = 'personal';
33
+ /** The `groupId` a tenant corresponds to, as handlers have always read it. */
34
+ const groupIdOf = (tenant) => tenant === exports.PERSONAL_TENANT ? null : tenant;
35
+ exports.groupIdOf = groupIdOf;
36
+ /** Just the groups, for the `req.userGroups` every handler already reads. */
37
+ const groupIdsOf = (tenants) => tenants.filter((tenant) => tenant !== exports.PERSONAL_TENANT);
38
+ exports.groupIdsOf = groupIdsOf;
39
+ /**
40
+ * Read a stored tenant grant, tolerating one written before tenants were a set.
41
+ *
42
+ * Keys and grants issued under the old single-tenant model carry `groupId`
43
+ * alone. Falling back to it here means they keep working across the deploy
44
+ * rather than every connected assistant breaking at once.
45
+ */
46
+ const readTenants = (stored) => {
47
+ const allowed = stored.tenants && stored.tenants.length > 0
48
+ ? stored.tenants
49
+ : [stored.groupId ?? exports.PERSONAL_TENANT];
50
+ const fallbackDefault = allowed[0];
51
+ const preferred = stored.defaultTenant ?? fallbackDefault;
52
+ return {
53
+ allowed,
54
+ default: allowed.includes(preferred) ? preferred : fallbackDefault
55
+ };
56
+ };
57
+ exports.readTenants = readTenants;
24
58
  //# sourceMappingURL=types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/apiKeys/types.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AAEH,+EAA+E;AAClE,QAAA,cAAc,GAAG;IAC5B,mBAAmB;IACnB,oBAAoB;IACpB,YAAY;IACZ,aAAa;IACb,cAAc;IACd,eAAe;CACP,CAAC;AAIJ,MAAM,aAAa,GAAG,CAAC,KAAc,EAAwB,EAAE,CACpE,OAAO,KAAK,KAAK,QAAQ,IAAK,sBAAoC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AADxE,QAAA,aAAa,iBAC2D"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/apiKeys/types.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AAEH,+EAA+E;AAClE,QAAA,cAAc,GAAG;IAC5B,mBAAmB;IACnB,oBAAoB;IACpB,YAAY;IACZ,aAAa;IACb,cAAc;IACd,eAAe;CACP,CAAC;AAIJ,MAAM,aAAa,GAAG,CAAC,KAAc,EAAwB,EAAE,CACpE,OAAO,KAAK,KAAK,QAAQ,IAAK,sBAAoC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AADxE,QAAA,aAAa,iBAC2D;AAErF;;;;;;;GAOG;AACU,QAAA,eAAe,GAAG,UAAU,CAAC;AAI1C,8EAA8E;AACvE,MAAM,SAAS,GAAG,CAAC,MAAc,EAAiB,EAAE,CACzD,MAAM,KAAK,uBAAe,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;AADhC,QAAA,SAAS,aACuB;AAoB7C,6EAA6E;AACtE,MAAM,UAAU,GAAG,CAAC,OAAiB,EAAY,EAAE,CACxD,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,uBAAe,CAAC,CAAC;AAD5C,QAAA,UAAU,cACkC;AAEzD;;;;;;GAMG;AACI,MAAM,WAAW,GAAG,CAAC,MAI3B,EAAgB,EAAE;IACjB,MAAM,OAAO,GACX,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;QACzC,CAAC,CAAC,MAAM,CAAC,OAAO;QAChB,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,IAAI,uBAAe,CAAC,CAAC;IAE1C,MAAM,eAAe,GAAG,OAAO,CAAC,CAAC,CAAW,CAAC;IAC7C,MAAM,SAAS,GAAG,MAAM,CAAC,aAAa,IAAI,eAAe,CAAC;IAE1D,OAAO;QACL,OAAO;QACP,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,eAAe;KACnE,CAAC;AACJ,CAAC,CAAC;AAjBW,QAAA,WAAW,eAiBtB"}
@@ -0,0 +1,51 @@
1
+ import mongoose, { Document } from 'mongoose';
2
+ import type { ActorKind } from './actor';
3
+ /**
4
+ * One thing that happened, and who did it.
5
+ *
6
+ * Lives in backend-core rather than any one service because the question it
7
+ * answers spans them: "what did Claude do yesterday" is not a relationship
8
+ * question or an album question. Every service writes into the same collection,
9
+ * and `service` says which one.
10
+ */
11
+ export interface IAuditEvent extends Document {
12
+ at: Date;
13
+ /** which service handled it — 'relationship-service', 'album-service' */
14
+ service: string;
15
+ action: 'create' | 'update' | 'delete' | 'read';
16
+ /** what kind of thing — 'activity', 'activity_type', 'photo' */
17
+ resource: string;
18
+ resourceId?: string;
19
+ actorKind: ActorKind;
20
+ /** the account acted for; every listing is scoped to this */
21
+ userId: string;
22
+ actorLabel: string;
23
+ /** a key's id or an OAuth client id; absent when a person acted directly */
24
+ actorCredentialId?: string;
25
+ /** the journal it happened in */
26
+ tenant: string;
27
+ /**
28
+ * What changed, field by field.
29
+ *
30
+ * Present on updates. This is the difference between an audit log and a
31
+ * request log: "Claude edited an entry" is barely worth storing, "Claude
32
+ * changed hours from 3 to 2" is the thing someone actually wants to see.
33
+ */
34
+ changes?: Record<string, {
35
+ from: unknown;
36
+ to: unknown;
37
+ }>;
38
+ /** what a delete removed, so it can be read back or restored by hand */
39
+ snapshot?: Record<string, unknown>;
40
+ /** how many records a read returned — the size of what left the server */
41
+ count?: number;
42
+ createdAt: Date;
43
+ }
44
+ export declare const AuditEvent: mongoose.Model<IAuditEvent, {}, {}, {}, mongoose.Document<unknown, {}, IAuditEvent, {}, mongoose.DefaultSchemaOptions> & IAuditEvent & Required<{
45
+ _id: mongoose.Types.ObjectId;
46
+ }> & {
47
+ __v: number;
48
+ } & {
49
+ id: string;
50
+ }, any, IAuditEvent>;
51
+ //# sourceMappingURL=AuditEvent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AuditEvent.d.ts","sourceRoot":"","sources":["../../src/audit/AuditEvent.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,EAAE,EAAE,QAAQ,EAAU,MAAM,UAAU,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC;;;;;;;GAOG;AACH,MAAM,WAAW,WAAY,SAAQ,QAAQ;IAC3C,EAAE,EAAE,IAAI,CAAC;IACT,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;IAChD,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,SAAS,CAAC;IACrB,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACzD,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;CACjB;AAoCD,eAAO,MAAM,UAAU;;;;;;oBAEwC,CAAC"}
@@ -0,0 +1,69 @@
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.AuditEvent = void 0;
37
+ const mongoose_1 = __importStar(require("mongoose"));
38
+ const AuditEventSchema = new mongoose_1.Schema({
39
+ at: { type: Date, required: true, default: Date.now },
40
+ service: { type: String, required: true },
41
+ action: { type: String, required: true, enum: ['create', 'update', 'delete', 'read'] },
42
+ resource: { type: String, required: true },
43
+ resourceId: { type: String },
44
+ actorKind: { type: String, required: true, enum: ['person', 'key', 'assistant'] },
45
+ userId: { type: String, required: true, index: true },
46
+ actorLabel: { type: String, required: true },
47
+ actorCredentialId: { type: String },
48
+ tenant: { type: String, required: true },
49
+ changes: { type: mongoose_1.Schema.Types.Mixed },
50
+ snapshot: { type: mongoose_1.Schema.Types.Mixed },
51
+ count: { type: Number }
52
+ }, { timestamps: { createdAt: true, updatedAt: false }, collection: 'audit_events' });
53
+ // Every listing is one account's trail, newest first.
54
+ AuditEventSchema.index({ userId: 1, at: -1 });
55
+ /**
56
+ * Rows expire on their own.
57
+ *
58
+ * They carry journal content — the old value of a description, a deleted
59
+ * entry — so keeping them indefinitely would quietly build a second copy of the
60
+ * journal that nothing in the product ever deletes from. A year is long enough
61
+ * to answer "what happened" and short enough that the copy does not outlive the
62
+ * question.
63
+ */
64
+ const RETENTION_DAYS = 365;
65
+ AuditEventSchema.index({ at: 1 }, { expireAfterSeconds: RETENTION_DAYS * 24 * 60 * 60 });
66
+ exports.AuditEvent = mongoose_1.default.models.AuditEvent
67
+ ? mongoose_1.default.models.AuditEvent
68
+ : mongoose_1.default.model('AuditEvent', AuditEventSchema);
69
+ //# sourceMappingURL=AuditEvent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AuditEvent.js","sourceRoot":"","sources":["../../src/audit/AuditEvent.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qDAAsD;AA0CtD,MAAM,gBAAgB,GAAG,IAAI,iBAAM,CACjC;IACE,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE;IACrD,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACzC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE;IACtF,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC1C,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;IAC5B,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,KAAK,EAAE,WAAW,CAAC,EAAE;IACjF,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;IACrD,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC5C,iBAAiB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;IACnC,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE;IACxC,OAAO,EAAE,EAAE,IAAI,EAAE,iBAAM,CAAC,KAAK,CAAC,KAAK,EAAE;IACrC,QAAQ,EAAE,EAAE,IAAI,EAAE,iBAAM,CAAC,KAAK,CAAC,KAAK,EAAE;IACtC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;CACxB,EACD,EAAE,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,UAAU,EAAE,cAAc,EAAE,CAClF,CAAC;AAEF,sDAAsD;AACtD,gBAAgB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;AAE9C;;;;;;;;GAQG;AACH,MAAM,cAAc,GAAG,GAAG,CAAC;AAC3B,gBAAgB,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,kBAAkB,EAAE,cAAc,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;AAE5E,QAAA,UAAU,GAAG,kBAAQ,CAAC,MAAM,CAAC,UAAU;IAClD,CAAC,CAAE,kBAAQ,CAAC,MAAM,CAAC,UAA0C;IAC7D,CAAC,CAAC,kBAAQ,CAAC,KAAK,CAAc,YAAY,EAAE,gBAAgB,CAAC,CAAC"}
@@ -0,0 +1,41 @@
1
+ import type { Request } from 'express';
2
+ import { type Tenant } from '../apiKeys/types';
3
+ /**
4
+ * Who is acting, in the only three ways anything reaches this system.
5
+ *
6
+ * The distinction that matters is `person` against everything else. A person is
7
+ * at a keyboard and can see what they are doing; a key or an assistant is
8
+ * software acting on their behalf, and is the thing an audit trail exists to
9
+ * make legible. Splitting `key` from `assistant` is worth the extra case because
10
+ * they fail differently: a key is a secret someone pasted somewhere and may have
11
+ * lost, an assistant is a consent screen someone approved and can withdraw.
12
+ */
13
+ export type ActorKind = 'person' | 'key' | 'assistant';
14
+ export interface Actor {
15
+ kind: ActorKind;
16
+ /** the account being acted for — the same whichever kind of actor it is */
17
+ userId: string;
18
+ /** what to show in a trail: the person's name, the key's name, the app's name */
19
+ label: string;
20
+ /**
21
+ * The credential, for software.
22
+ *
23
+ * A key's document id, or an OAuth client id. Deliberately not the access
24
+ * token's `jti`, which rotates hourly and would show a different actor after
25
+ * every refresh.
26
+ */
27
+ credentialId?: string;
28
+ /** the journal this request is acting in */
29
+ tenant: Tenant;
30
+ }
31
+ /**
32
+ * Read the actor off a request that has already been authenticated.
33
+ *
34
+ * Returns null for an unauthenticated request rather than inventing an
35
+ * anonymous actor: there is nothing to attribute, and a row saying so would be
36
+ * a row about the transport, not about anyone.
37
+ */
38
+ export declare function actorOf(req: Request): Actor | null;
39
+ /** One line naming an actor, for a log message or an error. */
40
+ export declare const describeActor: (actor: Actor) => string;
41
+ //# sourceMappingURL=actor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actor.d.ts","sourceRoot":"","sources":["../../src/audit/actor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAmB,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAEhE;;;;;;;;;GASG;AACH,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,KAAK,GAAG,WAAW,CAAC;AAEvD,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,KAAK,GAAG,IAAI,CAuBlD;AAED,+DAA+D;AAC/D,eAAO,MAAM,aAAa,GAAI,OAAO,KAAK,KAAG,MAC6B,CAAC"}
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.describeActor = void 0;
4
+ exports.actorOf = actorOf;
5
+ const types_1 = require("../apiKeys/types");
6
+ /**
7
+ * Read the actor off a request that has already been authenticated.
8
+ *
9
+ * Returns null for an unauthenticated request rather than inventing an
10
+ * anonymous actor: there is nothing to attribute, and a row saying so would be
11
+ * a row about the transport, not about anyone.
12
+ */
13
+ function actorOf(req) {
14
+ const user = req.user;
15
+ if (!user?.id)
16
+ return null;
17
+ const credential = req.apiKey;
18
+ if (!credential) {
19
+ return {
20
+ kind: 'person',
21
+ userId: user.id,
22
+ // Falling back to the email because a name is optional on a session and an
23
+ // unlabelled row is the one nobody can act on.
24
+ label: user.name || user.email || user.id,
25
+ tenant: req.query?.groupId || types_1.PERSONAL_TENANT
26
+ };
27
+ }
28
+ return {
29
+ kind: credential.kind === 'oauth' ? 'assistant' : 'key',
30
+ userId: user.id,
31
+ label: credential.label,
32
+ credentialId: credential.credentialId,
33
+ tenant: credential.actingAs
34
+ };
35
+ }
36
+ /** One line naming an actor, for a log message or an error. */
37
+ const describeActor = (actor) => actor.kind === 'person' ? actor.label : `${actor.label} (${actor.kind})`;
38
+ exports.describeActor = describeActor;
39
+ //# sourceMappingURL=actor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actor.js","sourceRoot":"","sources":["../../src/audit/actor.ts"],"names":[],"mappings":";;;AAwCA,0BAuBC;AA9DD,4CAAgE;AAgChE;;;;;;GAMG;AACH,SAAgB,OAAO,CAAC,GAAY;IAClC,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;IACtB,IAAI,CAAC,IAAI,EAAE,EAAE;QAAE,OAAO,IAAI,CAAC;IAE3B,MAAM,UAAU,GAAG,GAAG,CAAC,MAAM,CAAC;IAC9B,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO;YACL,IAAI,EAAE,QAAQ;YACd,MAAM,EAAE,IAAI,CAAC,EAAE;YACf,2EAA2E;YAC3E,+CAA+C;YAC/C,KAAK,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,EAAE;YACzC,MAAM,EAAG,GAAG,CAAC,KAAK,EAAE,OAAkB,IAAI,uBAAe;SAC1D,CAAC;IACJ,CAAC;IAED,OAAO;QACL,IAAI,EAAE,UAAU,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK;QACvD,MAAM,EAAE,IAAI,CAAC,EAAE;QACf,KAAK,EAAE,UAAU,CAAC,KAAK;QACvB,YAAY,EAAE,UAAU,CAAC,YAAY;QACrC,MAAM,EAAE,UAAU,CAAC,QAAQ;KAC5B,CAAC;AACJ,CAAC;AAED,+DAA+D;AACxD,MAAM,aAAa,GAAG,CAAC,KAAY,EAAU,EAAE,CACpD,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,IAAI,GAAG,CAAC;AAD9D,QAAA,aAAa,iBACiD"}
@@ -0,0 +1,40 @@
1
+ import type { NextFunction, Request, RequestHandler, Response } from 'express';
2
+ import { type Actor } from './actor';
3
+ /**
4
+ * The actor, reachable from wherever the write actually happens.
5
+ *
6
+ * Attribution has to come from the request and be recorded at the data layer,
7
+ * and those are far apart: a Mongoose hook knows exactly which fields changed
8
+ * and nothing about who asked. Threading an actor through every service and
9
+ * controller signature to bridge that would touch every function between the
10
+ * two and be silently wrong the first time someone forgot.
11
+ *
12
+ * `AsyncLocalStorage` carries it instead, so the hook reads the actor without
13
+ * anything in between knowing it exists. A write outside a request — a script,
14
+ * a migration — simply finds no actor, which is the truth about it.
15
+ */
16
+ export interface AuditContext {
17
+ actor: Actor;
18
+ service: string;
19
+ }
20
+ export declare const currentAuditContext: () => AuditContext | undefined;
21
+ /**
22
+ * Run the rest of a request with its actor in scope.
23
+ *
24
+ * Mounted after authentication, since there is no actor before it.
25
+ */
26
+ export declare const auditContext: (service: string) => RequestHandler;
27
+ /** Run something with an explicit actor — for jobs and tests. */
28
+ export declare const withAuditContext: <T>(context: AuditContext, fn: () => T) => T;
29
+ /**
30
+ * Put a request's actor in scope, and arm the read trail, for everything after.
31
+ *
32
+ * Called from `authenticateAgent` rather than mounted as its own middleware. A
33
+ * separate mount only works on a router that authenticates with
34
+ * `router.use(...)`; routers that authenticate per route — album's photos,
35
+ * finance's tickers — have no actor yet when a router-level middleware runs, so
36
+ * the context would be silently empty for exactly the endpoints most worth
37
+ * auditing. Doing it where authentication happens makes the two inseparable.
38
+ */
39
+ export declare function beginAudit(req: Request, res: Response, next: NextFunction): void;
40
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/audit/context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC/E,OAAO,EAAW,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAG9C;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,KAAK,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAID,eAAO,MAAM,mBAAmB,QAAO,YAAY,GAAG,SAA+B,CAAC;AAEtF;;;;GAIG;AACH,eAAO,MAAM,YAAY,GACtB,SAAS,MAAM,KAAG,cASlB,CAAC;AAEJ,iEAAiE;AACjE,eAAO,MAAM,gBAAgB,GAAI,CAAC,EAAE,SAAS,YAAY,EAAE,IAAI,MAAM,CAAC,KAAG,CAC/C,CAAC;AAiB3B;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAShF"}
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withAuditContext = exports.auditContext = exports.currentAuditContext = void 0;
4
+ exports.beginAudit = beginAudit;
5
+ const node_async_hooks_1 = require("node:async_hooks");
6
+ const actor_1 = require("./actor");
7
+ const reads_1 = require("./reads");
8
+ const storage = new node_async_hooks_1.AsyncLocalStorage();
9
+ const currentAuditContext = () => storage.getStore();
10
+ exports.currentAuditContext = currentAuditContext;
11
+ /**
12
+ * Run the rest of a request with its actor in scope.
13
+ *
14
+ * Mounted after authentication, since there is no actor before it.
15
+ */
16
+ const auditContext = (service) => (req, _res, next) => {
17
+ const actor = (0, actor_1.actorOf)(req);
18
+ if (!actor) {
19
+ next();
20
+ return;
21
+ }
22
+ storage.run({ actor, service }, next);
23
+ };
24
+ exports.auditContext = auditContext;
25
+ /** Run something with an explicit actor — for jobs and tests. */
26
+ const withAuditContext = (context, fn) => storage.run(context, fn);
27
+ exports.withAuditContext = withAuditContext;
28
+ /**
29
+ * The service this process is, for a trail that spans several of them.
30
+ *
31
+ * `SERVICE_NAME` is already how metrics, health and the internal-service clients
32
+ * identify a process, so auditing uses the same one rather than introducing a
33
+ * second name for the same thing.
34
+ *
35
+ * The fallback to the package name matters more here than elsewhere: the trail
36
+ * is filtered by service in the UI, and in local development nothing sets
37
+ * `SERVICE_NAME`, so every row would land under one meaningless label and the
38
+ * filter would look broken. Every service's package is named after it.
39
+ */
40
+ const serviceName = () => process.env.SERVICE_NAME || process.env.npm_package_name || 'unknown-service';
41
+ /**
42
+ * Put a request's actor in scope, and arm the read trail, for everything after.
43
+ *
44
+ * Called from `authenticateAgent` rather than mounted as its own middleware. A
45
+ * separate mount only works on a router that authenticates with
46
+ * `router.use(...)`; routers that authenticate per route — album's photos,
47
+ * finance's tickers — have no actor yet when a router-level middleware runs, so
48
+ * the context would be silently empty for exactly the endpoints most worth
49
+ * auditing. Doing it where authentication happens makes the two inseparable.
50
+ */
51
+ function beginAudit(req, res, next) {
52
+ const actor = (0, actor_1.actorOf)(req);
53
+ if (!actor) {
54
+ next();
55
+ return;
56
+ }
57
+ (0, reads_1.recordRead)(req, res, actor, serviceName());
58
+ storage.run({ actor, service: serviceName() }, next);
59
+ }
60
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/audit/context.ts"],"names":[],"mappings":";;;AAyEA,gCASC;AAlFD,uDAAqD;AAErD,mCAA8C;AAC9C,mCAAqC;AAoBrC,MAAM,OAAO,GAAG,IAAI,oCAAiB,EAAgB,CAAC;AAE/C,MAAM,mBAAmB,GAAG,GAA6B,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;AAAzE,QAAA,mBAAmB,uBAAsD;AAEtF;;;;GAIG;AACI,MAAM,YAAY,GACvB,CAAC,OAAe,EAAkB,EAAE,CACpC,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB,EAAQ,EAAE;IACzD,MAAM,KAAK,GAAG,IAAA,eAAO,EAAC,GAAG,CAAC,CAAC;IAC3B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,IAAI,EAAE,CAAC;QACP,OAAO;IACT,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,IAAI,CAAC,CAAC;AACxC,CAAC,CAAC;AAVS,QAAA,YAAY,gBAUrB;AAEJ,iEAAiE;AAC1D,MAAM,gBAAgB,GAAG,CAAI,OAAqB,EAAE,EAAW,EAAK,EAAE,CAC3E,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AADd,QAAA,gBAAgB,oBACF;AAE3B;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,GAAG,GAAW,EAAE,CAC/B,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,iBAAiB,CAAC;AAEhF;;;;;;;;;GASG;AACH,SAAgB,UAAU,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACxE,MAAM,KAAK,GAAG,IAAA,eAAO,EAAC,GAAG,CAAC,CAAC;IAC3B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,IAAI,EAAE,CAAC;QACP,OAAO;IACT,CAAC;IAED,IAAA,kBAAU,EAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;IAC3C,OAAO,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;AACvD,CAAC"}
@@ -0,0 +1,12 @@
1
+ export { actorOf, describeActor } from './actor';
2
+ export type { Actor, ActorKind } from './actor';
3
+ export { AuditEvent } from './AuditEvent';
4
+ export type { IAuditEvent } from './AuditEvent';
5
+ export { auditContext, beginAudit, currentAuditContext, withAuditContext } from './context';
6
+ export type { AuditContext } from './context';
7
+ export { auditPlugin } from './plugin';
8
+ export type { AuditPluginOptions } from './plugin';
9
+ export { auditReads, countRead, resourceOf } from './reads';
10
+ export { listAuditEvents } from './service';
11
+ export type { AuditQuery, AuditEntry } from './service';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/audit/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACjD,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC5F,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,YAAY,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC"}
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listAuditEvents = exports.resourceOf = exports.countRead = exports.auditReads = exports.auditPlugin = exports.withAuditContext = exports.currentAuditContext = exports.beginAudit = exports.auditContext = exports.AuditEvent = exports.describeActor = exports.actorOf = void 0;
4
+ var actor_1 = require("./actor");
5
+ Object.defineProperty(exports, "actorOf", { enumerable: true, get: function () { return actor_1.actorOf; } });
6
+ Object.defineProperty(exports, "describeActor", { enumerable: true, get: function () { return actor_1.describeActor; } });
7
+ var AuditEvent_1 = require("./AuditEvent");
8
+ Object.defineProperty(exports, "AuditEvent", { enumerable: true, get: function () { return AuditEvent_1.AuditEvent; } });
9
+ var context_1 = require("./context");
10
+ Object.defineProperty(exports, "auditContext", { enumerable: true, get: function () { return context_1.auditContext; } });
11
+ Object.defineProperty(exports, "beginAudit", { enumerable: true, get: function () { return context_1.beginAudit; } });
12
+ Object.defineProperty(exports, "currentAuditContext", { enumerable: true, get: function () { return context_1.currentAuditContext; } });
13
+ Object.defineProperty(exports, "withAuditContext", { enumerable: true, get: function () { return context_1.withAuditContext; } });
14
+ var plugin_1 = require("./plugin");
15
+ Object.defineProperty(exports, "auditPlugin", { enumerable: true, get: function () { return plugin_1.auditPlugin; } });
16
+ var reads_1 = require("./reads");
17
+ Object.defineProperty(exports, "auditReads", { enumerable: true, get: function () { return reads_1.auditReads; } });
18
+ Object.defineProperty(exports, "countRead", { enumerable: true, get: function () { return reads_1.countRead; } });
19
+ Object.defineProperty(exports, "resourceOf", { enumerable: true, get: function () { return reads_1.resourceOf; } });
20
+ var service_1 = require("./service");
21
+ Object.defineProperty(exports, "listAuditEvents", { enumerable: true, get: function () { return service_1.listAuditEvents; } });
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/audit/index.ts"],"names":[],"mappings":";;;AAAA,iCAAiD;AAAxC,gGAAA,OAAO,OAAA;AAAE,sGAAA,aAAa,OAAA;AAE/B,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;AAEnB,qCAA4F;AAAnF,uGAAA,YAAY,OAAA;AAAE,qGAAA,UAAU,OAAA;AAAE,8GAAA,mBAAmB,OAAA;AAAE,2GAAA,gBAAgB,OAAA;AAExE,mCAAuC;AAA9B,qGAAA,WAAW,OAAA;AAEpB,iCAA4D;AAAnD,mGAAA,UAAU,OAAA;AAAE,kGAAA,SAAS,OAAA;AAAE,mGAAA,UAAU,OAAA;AAC1C,qCAA4C;AAAnC,0GAAA,eAAe,OAAA"}
@@ -0,0 +1,23 @@
1
+ import type { Schema } from 'mongoose';
2
+ export interface AuditPluginOptions {
3
+ /** what these documents are called in a trail — 'activity', 'photo' */
4
+ resource: string;
5
+ /** fields never worth recording the content of */
6
+ redact?: string[];
7
+ }
8
+ /**
9
+ * Attribute and record every write to a collection.
10
+ *
11
+ * Applied to a model rather than called from its controllers, for two reasons.
12
+ * The data layer is the only place that knows which fields actually changed —
13
+ * a controller sees the patch that was requested, not the difference it made,
14
+ * and those differ whenever a field was already the value being set. And a
15
+ * plugin cannot be forgotten: a new endpoint that writes through the model is
16
+ * audited without anyone remembering to add a line.
17
+ *
18
+ * Writes made outside a request — a migration, a backfill script — find no
19
+ * actor and are recorded as nothing, which is honest. A row claiming a script
20
+ * was a person would be worse than no row.
21
+ */
22
+ export declare function auditPlugin(schema: Schema, options: AuditPluginOptions): void;
23
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/audit/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,EAAE,MAAM,UAAU,CAAC;AA6D9C,MAAM,WAAW,kBAAkB;IACjC,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,IAAI,CA2K7E"}