@headless-lms/core 0.1.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 (154) hide show
  1. package/dist/assets/events.d.ts +66 -3
  2. package/dist/assets/events.js +19 -3
  3. package/dist/assets/events.js.map +1 -1
  4. package/dist/assets/service.js +5 -3
  5. package/dist/assets/service.js.map +1 -1
  6. package/dist/automations/actions.d.ts +11 -3
  7. package/dist/automations/actions.js +38 -5
  8. package/dist/automations/actions.js.map +1 -1
  9. package/dist/automations/catalog.d.ts +4 -1
  10. package/dist/automations/catalog.js +289 -188
  11. package/dist/automations/catalog.js.map +1 -1
  12. package/dist/automations/events.d.ts +46 -24
  13. package/dist/automations/events.js +5 -5
  14. package/dist/automations/events.js.map +1 -1
  15. package/dist/automations/index.d.ts +5 -5
  16. package/dist/automations/index.js +2 -2
  17. package/dist/automations/model.d.ts +13 -3
  18. package/dist/automations/model.js +23 -3
  19. package/dist/automations/model.js.map +1 -1
  20. package/dist/automations/ports.d.ts +23 -8
  21. package/dist/automations/service.d.ts +21 -5
  22. package/dist/automations/service.js +267 -47
  23. package/dist/automations/service.js.map +1 -1
  24. package/dist/automations/types.d.ts +2 -2
  25. package/dist/automations/webhook-signing.d.ts +7 -0
  26. package/dist/automations/webhook-signing.js +22 -0
  27. package/dist/automations/webhook-signing.js.map +1 -0
  28. package/dist/content/events.d.ts +401 -25
  29. package/dist/content/events.js +125 -19
  30. package/dist/content/events.js.map +1 -1
  31. package/dist/content/index.d.ts +5 -5
  32. package/dist/content/index.js +2 -2
  33. package/dist/content/model.d.ts +2 -2
  34. package/dist/content/ports.d.ts +42 -11
  35. package/dist/content/service.d.ts +15 -9
  36. package/dist/content/service.js +153 -45
  37. package/dist/content/service.js.map +1 -1
  38. package/dist/content/types.d.ts +2 -2
  39. package/dist/discussion/events.d.ts +60 -4
  40. package/dist/discussion/events.js +30 -4
  41. package/dist/discussion/events.js.map +1 -1
  42. package/dist/discussion/model.d.ts +1 -2
  43. package/dist/discussion/ports.d.ts +1 -1
  44. package/dist/discussion/service.d.ts +1 -1
  45. package/dist/discussion/service.js +11 -10
  46. package/dist/discussion/service.js.map +1 -1
  47. package/dist/entitlements/events.d.ts +96 -12
  48. package/dist/entitlements/events.js +21 -4
  49. package/dist/entitlements/events.js.map +1 -1
  50. package/dist/entitlements/service.js +1 -0
  51. package/dist/entitlements/service.js.map +1 -1
  52. package/dist/identity/events.d.ts +17 -3
  53. package/dist/identity/events.js +12 -2
  54. package/dist/identity/events.js.map +1 -1
  55. package/dist/identity/ports.d.ts +3 -0
  56. package/dist/identity/service.d.ts +3 -0
  57. package/dist/identity/service.js +12 -1
  58. package/dist/identity/service.js.map +1 -1
  59. package/dist/integrations/events.d.ts +27 -3
  60. package/dist/integrations/events.js +15 -3
  61. package/dist/integrations/events.js.map +1 -1
  62. package/dist/integrations/service.js +1 -1
  63. package/dist/integrations/service.js.map +1 -1
  64. package/dist/organizations/events.d.ts +178 -11
  65. package/dist/organizations/events.js +52 -9
  66. package/dist/organizations/events.js.map +1 -1
  67. package/dist/organizations/index.d.ts +3 -3
  68. package/dist/organizations/members.js.map +1 -1
  69. package/dist/organizations/ports.d.ts +6 -10
  70. package/dist/organizations/roles.js.map +1 -1
  71. package/dist/organizations/service.d.ts +9 -4
  72. package/dist/organizations/service.js +131 -72
  73. package/dist/organizations/service.js.map +1 -1
  74. package/dist/organizations/types.d.ts +2 -2
  75. package/dist/progress/events.d.ts +34 -2
  76. package/dist/progress/events.js +15 -2
  77. package/dist/progress/events.js.map +1 -1
  78. package/dist/progress/service.d.ts +2 -2
  79. package/dist/progress/service.js +4 -2
  80. package/dist/progress/service.js.map +1 -1
  81. package/dist/reporting/courses/index.d.ts +4 -0
  82. package/dist/reporting/courses/index.js +2 -0
  83. package/dist/reporting/courses/model.d.ts +24 -0
  84. package/dist/reporting/courses/model.js +1 -0
  85. package/dist/reporting/courses/ports.d.ts +13 -0
  86. package/dist/reporting/courses/ports.js +1 -0
  87. package/dist/reporting/courses/service.d.ts +19 -0
  88. package/dist/reporting/courses/service.js +20 -0
  89. package/dist/reporting/courses/service.js.map +1 -0
  90. package/dist/reporting/learn/model.d.ts +2 -2
  91. package/dist/reporting/learn/ports.d.ts +2 -1
  92. package/dist/reporting/learn/service.d.ts +2 -1
  93. package/dist/reporting/learn/service.js +5 -0
  94. package/dist/reporting/learn/service.js.map +1 -1
  95. package/dist/reporting/students/index.d.ts +2 -2
  96. package/dist/reporting/students/model.d.ts +21 -1
  97. package/dist/reporting/students/ports.d.ts +3 -1
  98. package/dist/reporting/students/service.d.ts +2 -1
  99. package/dist/reporting/students/service.js +3 -0
  100. package/dist/reporting/students/service.js.map +1 -1
  101. package/dist/shared/events.d.ts +5 -3
  102. package/dist/shared/events.js +1 -0
  103. package/dist/shared/events.js.map +1 -1
  104. package/dist/shared/id.d.ts +1 -0
  105. package/dist/shared/id.js +1 -0
  106. package/dist/shared/id.js.map +1 -1
  107. package/dist/shared/mailer.d.ts +2 -0
  108. package/dist/shared/mailer.js.map +1 -1
  109. package/dist/shared/settings.js.map +1 -1
  110. package/dist/types/automations.d.ts +13 -2
  111. package/dist/types/content.d.ts +2 -2
  112. package/dist/types/discussion.d.ts +3 -2
  113. package/dist/types/email-templates.d.ts +3 -1
  114. package/dist/types/events.d.ts +3 -3
  115. package/dist/types/index.d.ts +6 -6
  116. package/dist/types/organizations.d.ts +2 -2
  117. package/dist/types/ports.d.ts +1 -1
  118. package/dist/types/schemas/assets.d.ts +3 -0
  119. package/dist/types/schemas/assets.js +2 -1
  120. package/dist/types/schemas/assets.js.map +1 -1
  121. package/dist/types/schemas/automations.d.ts +71 -4
  122. package/dist/types/schemas/automations.js +35 -4
  123. package/dist/types/schemas/automations.js.map +1 -1
  124. package/dist/types/schemas/content.d.ts +51 -18
  125. package/dist/types/schemas/content.js +40 -13
  126. package/dist/types/schemas/content.js.map +1 -1
  127. package/dist/types/schemas/credentials.js.map +1 -1
  128. package/dist/types/schemas/discussion.d.ts +10 -2
  129. package/dist/types/schemas/discussion.js +12 -5
  130. package/dist/types/schemas/discussion.js.map +1 -1
  131. package/dist/types/schemas/entitlements.d.ts +8 -2
  132. package/dist/types/schemas/entitlements.js +10 -4
  133. package/dist/types/schemas/entitlements.js.map +1 -1
  134. package/dist/types/schemas/identity.d.ts +4 -4
  135. package/dist/types/schemas/identity.js +5 -5
  136. package/dist/types/schemas/identity.js.map +1 -1
  137. package/dist/types/schemas/index.d.ts +6 -6
  138. package/dist/types/schemas/index.js +6 -6
  139. package/dist/types/schemas/integrations.js.map +1 -1
  140. package/dist/types/schemas/organizations.d.ts +19 -5
  141. package/dist/types/schemas/organizations.js +12 -7
  142. package/dist/types/schemas/organizations.js.map +1 -1
  143. package/dist/types/schemas/outbox.d.ts +1 -0
  144. package/dist/types/schemas/outbox.js +1 -0
  145. package/dist/types/schemas/outbox.js.map +1 -1
  146. package/dist/types/schemas/progress.d.ts +1 -0
  147. package/dist/types/schemas/progress.js +1 -0
  148. package/dist/types/schemas/progress.js.map +1 -1
  149. package/dist/types/schemas/settings.js.map +1 -1
  150. package/dist/types/schemas/shared.d.ts +2 -1
  151. package/dist/types/schemas/shared.js +3 -1
  152. package/dist/types/schemas/shared.js.map +1 -1
  153. package/dist/types/shared.d.ts +1 -1
  154. package/package.json +1 -1
@@ -13,6 +13,7 @@ declare const assetEvents: {
13
13
  status: "pending" | "ready";
14
14
  uploadedBy: string;
15
15
  createdAt: Date;
16
+ updatedAt: Date;
16
17
  }, "asset.created", 1>, {
17
18
  orgId: string;
18
19
  id: string;
@@ -24,7 +25,27 @@ declare const assetEvents: {
24
25
  status: "pending" | "ready";
25
26
  uploadedBy: string;
26
27
  createdAt: unknown;
27
- }>;
28
+ updatedAt: unknown;
29
+ }, import("zod").ZodObject<{
30
+ orgId: import("zod").ZodString;
31
+ id: import("zod").ZodString;
32
+ kind: import("zod").ZodEnum<{
33
+ video: "video";
34
+ download: "download";
35
+ content: "content";
36
+ }>;
37
+ createdAt: import("zod").ZodCoercedDate<unknown>;
38
+ updatedAt: import("zod").ZodCoercedDate<unknown>;
39
+ status: import("zod").ZodEnum<{
40
+ pending: "pending";
41
+ ready: "ready";
42
+ }>;
43
+ uploadedBy: import("zod").ZodString;
44
+ key: import("zod").ZodString;
45
+ filename: import("zod").ZodString;
46
+ contentType: import("zod").ZodString;
47
+ size: import("zod").ZodNumber;
48
+ }, import("zod/v4/core").$strip>>;
28
49
  assetReady: RuntimeEventDefinition<VersionedDomainEvent<{
29
50
  orgId: string;
30
51
  id: string;
@@ -36,6 +57,7 @@ declare const assetEvents: {
36
57
  status: "pending" | "ready";
37
58
  uploadedBy: string;
38
59
  createdAt: Date;
60
+ updatedAt: Date;
39
61
  }, "asset.ready", 1>, {
40
62
  orgId: string;
41
63
  id: string;
@@ -47,7 +69,27 @@ declare const assetEvents: {
47
69
  status: "pending" | "ready";
48
70
  uploadedBy: string;
49
71
  createdAt: unknown;
50
- }>;
72
+ updatedAt: unknown;
73
+ }, import("zod").ZodObject<{
74
+ orgId: import("zod").ZodString;
75
+ id: import("zod").ZodString;
76
+ kind: import("zod").ZodEnum<{
77
+ video: "video";
78
+ download: "download";
79
+ content: "content";
80
+ }>;
81
+ createdAt: import("zod").ZodCoercedDate<unknown>;
82
+ updatedAt: import("zod").ZodCoercedDate<unknown>;
83
+ status: import("zod").ZodEnum<{
84
+ pending: "pending";
85
+ ready: "ready";
86
+ }>;
87
+ uploadedBy: import("zod").ZodString;
88
+ key: import("zod").ZodString;
89
+ filename: import("zod").ZodString;
90
+ contentType: import("zod").ZodString;
91
+ size: import("zod").ZodNumber;
92
+ }, import("zod/v4/core").$strip>>;
51
93
  assetDeleted: RuntimeEventDefinition<VersionedDomainEvent<{
52
94
  orgId: string;
53
95
  id: string;
@@ -59,6 +101,7 @@ declare const assetEvents: {
59
101
  status: "pending" | "ready";
60
102
  uploadedBy: string;
61
103
  createdAt: Date;
104
+ updatedAt: Date;
62
105
  }, "asset.deleted", 1>, {
63
106
  orgId: string;
64
107
  id: string;
@@ -70,7 +113,27 @@ declare const assetEvents: {
70
113
  status: "pending" | "ready";
71
114
  uploadedBy: string;
72
115
  createdAt: unknown;
73
- }>;
116
+ updatedAt: unknown;
117
+ }, import("zod").ZodObject<{
118
+ orgId: import("zod").ZodString;
119
+ id: import("zod").ZodString;
120
+ kind: import("zod").ZodEnum<{
121
+ video: "video";
122
+ download: "download";
123
+ content: "content";
124
+ }>;
125
+ createdAt: import("zod").ZodCoercedDate<unknown>;
126
+ updatedAt: import("zod").ZodCoercedDate<unknown>;
127
+ status: import("zod").ZodEnum<{
128
+ pending: "pending";
129
+ ready: "ready";
130
+ }>;
131
+ uploadedBy: import("zod").ZodString;
132
+ key: import("zod").ZodString;
133
+ filename: import("zod").ZodString;
134
+ contentType: import("zod").ZodString;
135
+ size: import("zod").ZodNumber;
136
+ }, import("zod/v4/core").$strip>>;
74
137
  };
75
138
  type AssetCreated = EventOf<typeof assetEvents.assetCreated>;
76
139
  type AssetReady = EventOf<typeof assetEvents.assetReady>;
@@ -1,21 +1,37 @@
1
1
  import { assetSchema } from "../types/schemas/assets.js";
2
2
  import { defineEvent } from "../shared/events.js";
3
3
  //#region src/assets/events.ts
4
+ const publicAsset = assetSchema.pick({
5
+ orgId: true,
6
+ id: true,
7
+ key: true,
8
+ kind: true,
9
+ filename: true,
10
+ contentType: true,
11
+ size: true,
12
+ status: true,
13
+ uploadedBy: true,
14
+ createdAt: true,
15
+ updatedAt: true
16
+ }).strip();
4
17
  const assetEvents = {
5
18
  assetCreated: defineEvent({
6
19
  type: "asset.created",
7
20
  version: 1,
8
- data: assetSchema
21
+ data: assetSchema,
22
+ publicData: publicAsset
9
23
  }),
10
24
  assetReady: defineEvent({
11
25
  type: "asset.ready",
12
26
  version: 1,
13
- data: assetSchema
27
+ data: assetSchema,
28
+ publicData: publicAsset
14
29
  }),
15
30
  assetDeleted: defineEvent({
16
31
  type: "asset.deleted",
17
32
  version: 1,
18
- data: assetSchema
33
+ data: assetSchema,
34
+ publicData: publicAsset
19
35
  })
20
36
  };
21
37
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"events.js","names":[],"sources":["../../src/assets/events.ts"],"sourcesContent":["import { assetSchema } from '../types/schemas/index.js';\nimport { defineEvent, type EventOf, type EventOfValues } from '../shared/ports.js';\n\nexport const assetEvents = {\n assetCreated: defineEvent({\n type: 'asset.created',\n version: 1,\n data: assetSchema,\n }),\n assetReady: defineEvent({\n type: 'asset.ready',\n version: 1,\n data: assetSchema,\n }),\n assetDeleted: defineEvent({\n type: 'asset.deleted',\n version: 1,\n data: assetSchema,\n }),\n};\n\nexport type AssetCreated = EventOf<typeof assetEvents.assetCreated>;\nexport type AssetReady = EventOf<typeof assetEvents.assetReady>;\nexport type AssetDeleted = EventOf<typeof assetEvents.assetDeleted>;\nexport type AssetEvent = EventOfValues<typeof assetEvents>;\n"],"mappings":";;;AAGA,MAAa,cAAc;CACzB,cAAc,YAAY;EACxB,MAAM;EACN,SAAS;EACT,MAAM;CACR,CAAC;CACD,YAAY,YAAY;EACtB,MAAM;EACN,SAAS;EACT,MAAM;CACR,CAAC;CACD,cAAc,YAAY;EACxB,MAAM;EACN,SAAS;EACT,MAAM;CACR,CAAC;AACH"}
1
+ {"version":3,"file":"events.js","names":[],"sources":["../../src/assets/events.ts"],"sourcesContent":["import { assetSchema } from '../types/schemas/index.js';\nimport { defineEvent, type EventOf, type EventOfValues } from '../shared/ports.js';\n\nconst publicAsset = assetSchema\n .pick({\n orgId: true,\n id: true,\n key: true,\n kind: true,\n filename: true,\n contentType: true,\n size: true,\n status: true,\n uploadedBy: true,\n createdAt: true,\n updatedAt: true,\n })\n .strip();\n\nexport const assetEvents = {\n assetCreated: defineEvent({\n type: 'asset.created',\n version: 1,\n data: assetSchema,\n publicData: publicAsset,\n }),\n assetReady: defineEvent({\n type: 'asset.ready',\n version: 1,\n data: assetSchema,\n publicData: publicAsset,\n }),\n assetDeleted: defineEvent({\n type: 'asset.deleted',\n version: 1,\n data: assetSchema,\n publicData: publicAsset,\n }),\n};\n\nexport type AssetCreated = EventOf<typeof assetEvents.assetCreated>;\nexport type AssetReady = EventOf<typeof assetEvents.assetReady>;\nexport type AssetDeleted = EventOf<typeof assetEvents.assetDeleted>;\nexport type AssetEvent = EventOfValues<typeof assetEvents>;\n"],"mappings":";;;AAGA,MAAM,cAAc,YACjB,KAAK;CACJ,OAAO;CACP,IAAI;CACJ,KAAK;CACL,MAAM;CACN,UAAU;CACV,aAAa;CACb,MAAM;CACN,QAAQ;CACR,YAAY;CACZ,WAAW;CACX,WAAW;AACb,CAAC,CAAC,CACD,MAAM;AAET,MAAa,cAAc;CACzB,cAAc,YAAY;EACxB,MAAM;EACN,SAAS;EACT,MAAM;EACN,YAAY;CACd,CAAC;CACD,YAAY,YAAY;EACtB,MAAM;EACN,SAAS;EACT,MAAM;EACN,YAAY;CACd,CAAC;CACD,cAAc,YAAY;EACxB,MAAM;EACN,SAAS;EACT,MAAM;EACN,YAAY;CACd,CAAC;AACH"}
@@ -1,6 +1,6 @@
1
- import { assetEvents } from "./events.js";
2
- import { genId } from "../shared/id.js";
3
1
  import { noopLogger } from "../shared/logger.js";
2
+ import { genId } from "../shared/id.js";
3
+ import { assetEvents } from "./events.js";
4
4
  //#region src/assets/service.ts
5
5
  function orgPrefix(orgId) {
6
6
  return `org/${orgId}/`;
@@ -26,6 +26,7 @@ var AssetsServiceImpl = class {
26
26
  key,
27
27
  contentType: input.contentType
28
28
  });
29
+ const now = /* @__PURE__ */ new Date();
29
30
  const asset = await this.uow.run(async ({ assets, outbox }) => {
30
31
  const asset = await assets.insert(orgId, {
31
32
  orgId,
@@ -37,7 +38,8 @@ var AssetsServiceImpl = class {
37
38
  size: 0,
38
39
  status: "pending",
39
40
  uploadedBy: input.uploadedBy,
40
- createdAt: /* @__PURE__ */ new Date()
41
+ createdAt: now,
42
+ updatedAt: now
41
43
  });
42
44
  await outbox.append([assetEvents.assetCreated.make({
43
45
  orgId,
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","names":[],"sources":["../../src/assets/service.ts"],"sourcesContent":["// assets context — service. Owns org-scoped key construction and the asset\n// lifecycle (register → upload → confirm → serve → remove); delegates the\n// actual object operations to the ObjectStorage port. Org isolation is enforced\n// by scoping every lookup to the caller's org (cross-org reads return null).\nimport { genId } from '../shared/id.js';\nimport type { Logger, ObjectStorage } from '../shared/ports.js';\nimport type {\n Asset,\n AssetsQuery,\n DownloadTicket,\n Page,\n RequestUploadInput,\n UploadTicket,\n} from './model.js';\nimport type { AssetsRepository, AssetsService, AssetsUnitOfWork } from './ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { assetEvents } from './events.js';\n\nfunction orgPrefix(orgId: string): string {\n return `org/${orgId}/`;\n}\n\nfunction sanitizeFilename(filename: string): string {\n const base = filename.split(/[\\\\/]/).pop() ?? 'file';\n return base.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 120) || 'file';\n}\n\nexport type AssetsServiceParams = {\n storage: ObjectStorage;\n repo: AssetsRepository;\n uow: AssetsUnitOfWork;\n logger?: Logger;\n};\n\nexport class AssetsServiceImpl implements AssetsService {\n private readonly storage: ObjectStorage;\n private readonly repo: AssetsRepository;\n private readonly uow: AssetsUnitOfWork;\n private readonly logger: Logger;\n\n constructor(params: AssetsServiceParams) {\n this.storage = params.storage;\n this.repo = params.repo;\n this.uow = params.uow;\n this.logger = params.logger ?? noopLogger;\n }\n\n async requestUpload(orgId: string, input: RequestUploadInput): Promise<UploadTicket> {\n const id = genId('asset');\n const key = `${orgPrefix(orgId)}${input.kind}/${id}/${sanitizeFilename(input.filename)}`;\n const presigned = await this.storage.presignUpload({ key, contentType: input.contentType });\n const asset = await this.uow.run(async ({ assets, outbox }) => {\n const asset = await assets.insert(orgId, {\n orgId,\n id,\n key,\n kind: input.kind,\n filename: input.filename,\n contentType: input.contentType,\n size: 0,\n status: 'pending',\n uploadedBy: input.uploadedBy,\n createdAt: new Date(),\n });\n await outbox.append([assetEvents.assetCreated.make({ orgId, data: asset })]);\n return asset;\n });\n this.logger.info('asset upload requested', { orgId, assetId: id, kind: input.kind });\n return {\n asset,\n uploadUrl: presigned.url,\n method: 'PUT',\n expiresInSeconds: presigned.expiresInSeconds,\n headers: presigned.headers,\n };\n }\n\n async confirm(orgId: string, id: string): Promise<Asset | null> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n return null;\n }\n const stat = await this.storage.stat(asset.key);\n if (!stat) {\n this.logger.debug('asset not yet uploaded', { orgId, assetId: id });\n return asset; // not uploaded yet — stays pending\n }\n const updated = await this.uow.run(async ({ assets, outbox }) => {\n const updated = await assets.update(id, {\n size: stat.size,\n contentType: stat.contentType ?? asset.contentType,\n status: 'ready',\n });\n if (updated) {\n await outbox.append([assetEvents.assetReady.make({ orgId, data: updated })]);\n }\n return updated;\n });\n this.logger.info('asset confirmed', { orgId, assetId: id });\n return updated;\n }\n\n list(orgId: string, query: AssetsQuery): Promise<Page<Asset>> {\n return this.repo.list(orgId, query);\n }\n\n get(orgId: string, id: string): Promise<Asset | null> {\n return this.repo.findById(orgId, id);\n }\n\n async requestDownload(\n orgId: string,\n id: string,\n filename?: string,\n expiresInSeconds?: number,\n ): Promise<DownloadTicket | null> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n return null;\n }\n const url = await this.storage.presignDownload({\n key: asset.key,\n downloadFilename: filename ?? asset.filename,\n ...(expiresInSeconds === undefined ? {} : { expiresInSeconds }),\n });\n return { url, asset };\n }\n\n async remove(orgId: string, id: string): Promise<boolean> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n return false;\n }\n await this.storage.remove(asset.key);\n const deleted = await this.uow.run(async ({ assets, outbox }) => {\n const deleted = await assets.delete(id);\n if (deleted) {\n await outbox.append([assetEvents.assetDeleted.make({ orgId, data: asset })]);\n }\n return deleted;\n });\n this.logger.info('asset removed', { orgId, assetId: id });\n return deleted;\n }\n}\n"],"mappings":";;;;AAkBA,SAAS,UAAU,OAAuB;CACxC,OAAO,OAAO,MAAM;AACtB;AAEA,SAAS,iBAAiB,UAA0B;CAElD,QADa,SAAS,MAAM,OAAO,CAAC,CAAC,IAAI,KAAK,OAAA,CAClC,QAAQ,oBAAoB,GAAG,CAAC,CAAC,MAAM,GAAG,GAAG,KAAK;AAChE;AASA,IAAa,oBAAb,MAAwD;CACtD;CACA;CACA;CACA;CAEA,YAAY,QAA6B;EACvC,KAAK,UAAU,OAAO;EACtB,KAAK,OAAO,OAAO;EACnB,KAAK,MAAM,OAAO;EAClB,KAAK,SAAS,OAAO,UAAU;CACjC;CAEA,MAAM,cAAc,OAAe,OAAkD;EACnF,MAAM,KAAK,MAAM,OAAO;EACxB,MAAM,MAAM,GAAG,UAAU,KAAK,IAAI,MAAM,KAAK,GAAG,GAAG,GAAG,iBAAiB,MAAM,QAAQ;EACrF,MAAM,YAAY,MAAM,KAAK,QAAQ,cAAc;GAAE;GAAK,aAAa,MAAM;EAAY,CAAC;EAC1F,MAAM,QAAQ,MAAM,KAAK,IAAI,IAAI,OAAO,EAAE,QAAQ,aAAa;GAC7D,MAAM,QAAQ,MAAM,OAAO,OAAO,OAAO;IACvC;IACA;IACA;IACA,MAAM,MAAM;IACZ,UAAU,MAAM;IAChB,aAAa,MAAM;IACnB,MAAM;IACN,QAAQ;IACR,YAAY,MAAM;IAClB,2BAAW,IAAI,KAAK;GACtB,CAAC;GACD,MAAM,OAAO,OAAO,CAAC,YAAY,aAAa,KAAK;IAAE;IAAO,MAAM;GAAM,CAAC,CAAC,CAAC;GAC3E,OAAO;EACT,CAAC;EACD,KAAK,OAAO,KAAK,0BAA0B;GAAE;GAAO,SAAS;GAAI,MAAM,MAAM;EAAK,CAAC;EACnF,OAAO;GACL;GACA,WAAW,UAAU;GACrB,QAAQ;GACR,kBAAkB,UAAU;GAC5B,SAAS,UAAU;EACrB;CACF;CAEA,MAAM,QAAQ,OAAe,IAAmC;EAC9D,MAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,OAAO,EAAE;EAChD,IAAI,CAAC,OACH,OAAO;EAET,MAAM,OAAO,MAAM,KAAK,QAAQ,KAAK,MAAM,GAAG;EAC9C,IAAI,CAAC,MAAM;GACT,KAAK,OAAO,MAAM,0BAA0B;IAAE;IAAO,SAAS;GAAG,CAAC;GAClE,OAAO;EACT;EACA,MAAM,UAAU,MAAM,KAAK,IAAI,IAAI,OAAO,EAAE,QAAQ,aAAa;GAC/D,MAAM,UAAU,MAAM,OAAO,OAAO,IAAI;IACtC,MAAM,KAAK;IACX,aAAa,KAAK,eAAe,MAAM;IACvC,QAAQ;GACV,CAAC;GACD,IAAI,SACF,MAAM,OAAO,OAAO,CAAC,YAAY,WAAW,KAAK;IAAE;IAAO,MAAM;GAAQ,CAAC,CAAC,CAAC;GAE7E,OAAO;EACT,CAAC;EACD,KAAK,OAAO,KAAK,mBAAmB;GAAE;GAAO,SAAS;EAAG,CAAC;EAC1D,OAAO;CACT;CAEA,KAAK,OAAe,OAA0C;EAC5D,OAAO,KAAK,KAAK,KAAK,OAAO,KAAK;CACpC;CAEA,IAAI,OAAe,IAAmC;EACpD,OAAO,KAAK,KAAK,SAAS,OAAO,EAAE;CACrC;CAEA,MAAM,gBACJ,OACA,IACA,UACA,kBACgC;EAChC,MAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,OAAO,EAAE;EAChD,IAAI,CAAC,OACH,OAAO;EAOT,OAAO;GAAE,KAAA,MALS,KAAK,QAAQ,gBAAgB;IAC7C,KAAK,MAAM;IACX,kBAAkB,YAAY,MAAM;IACpC,GAAI,qBAAqB,KAAA,IAAY,CAAC,IAAI,EAAE,iBAAiB;GAC/D,CAAC;GACa;EAAM;CACtB;CAEA,MAAM,OAAO,OAAe,IAA8B;EACxD,MAAM,QAAQ,MAAM,KAAK,KAAK,SAAS,OAAO,EAAE;EAChD,IAAI,CAAC,OACH,OAAO;EAET,MAAM,KAAK,QAAQ,OAAO,MAAM,GAAG;EACnC,MAAM,UAAU,MAAM,KAAK,IAAI,IAAI,OAAO,EAAE,QAAQ,aAAa;GAC/D,MAAM,UAAU,MAAM,OAAO,OAAO,EAAE;GACtC,IAAI,SACF,MAAM,OAAO,OAAO,CAAC,YAAY,aAAa,KAAK;IAAE;IAAO,MAAM;GAAM,CAAC,CAAC,CAAC;GAE7E,OAAO;EACT,CAAC;EACD,KAAK,OAAO,KAAK,iBAAiB;GAAE;GAAO,SAAS;EAAG,CAAC;EACxD,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"service.js","names":[],"sources":["../../src/assets/service.ts"],"sourcesContent":["// assets context — service. Owns org-scoped key construction and the asset\n// lifecycle (register → upload → confirm → serve → remove); delegates the\n// actual object operations to the ObjectStorage port. Org isolation is enforced\n// by scoping every lookup to the caller's org (cross-org reads return null).\nimport { genId } from '../shared/id.js';\nimport type { Logger, ObjectStorage } from '../shared/ports.js';\nimport type {\n Asset,\n AssetsQuery,\n DownloadTicket,\n Page,\n RequestUploadInput,\n UploadTicket,\n} from './model.js';\nimport type { AssetsRepository, AssetsService, AssetsUnitOfWork } from './ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { assetEvents } from './events.js';\n\nfunction orgPrefix(orgId: string): string {\n return `org/${orgId}/`;\n}\n\nfunction sanitizeFilename(filename: string): string {\n const base = filename.split(/[\\\\/]/).pop() ?? 'file';\n return base.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 120) || 'file';\n}\n\nexport type AssetsServiceParams = {\n storage: ObjectStorage;\n repo: AssetsRepository;\n uow: AssetsUnitOfWork;\n logger?: Logger;\n};\n\nexport class AssetsServiceImpl implements AssetsService {\n private readonly storage: ObjectStorage;\n private readonly repo: AssetsRepository;\n private readonly uow: AssetsUnitOfWork;\n private readonly logger: Logger;\n\n constructor(params: AssetsServiceParams) {\n this.storage = params.storage;\n this.repo = params.repo;\n this.uow = params.uow;\n this.logger = params.logger ?? noopLogger;\n }\n\n async requestUpload(orgId: string, input: RequestUploadInput): Promise<UploadTicket> {\n const id = genId('asset');\n const key = `${orgPrefix(orgId)}${input.kind}/${id}/${sanitizeFilename(input.filename)}`;\n const presigned = await this.storage.presignUpload({ key, contentType: input.contentType });\n const now = new Date();\n const asset = await this.uow.run(async ({ assets, outbox }) => {\n const asset = await assets.insert(orgId, {\n orgId,\n id,\n key,\n kind: input.kind,\n filename: input.filename,\n contentType: input.contentType,\n size: 0,\n status: 'pending',\n uploadedBy: input.uploadedBy,\n createdAt: now,\n updatedAt: now,\n });\n await outbox.append([assetEvents.assetCreated.make({ orgId, data: asset })]);\n return asset;\n });\n this.logger.info('asset upload requested', { orgId, assetId: id, kind: input.kind });\n return {\n asset,\n uploadUrl: presigned.url,\n method: 'PUT',\n expiresInSeconds: presigned.expiresInSeconds,\n headers: presigned.headers,\n };\n }\n\n async confirm(orgId: string, id: string): Promise<Asset | null> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n return null;\n }\n const stat = await this.storage.stat(asset.key);\n if (!stat) {\n this.logger.debug('asset not yet uploaded', { orgId, assetId: id });\n return asset; // not uploaded yet — stays pending\n }\n const updated = await this.uow.run(async ({ assets, outbox }) => {\n const updated = await assets.update(id, {\n size: stat.size,\n contentType: stat.contentType ?? asset.contentType,\n status: 'ready',\n });\n if (updated) {\n await outbox.append([assetEvents.assetReady.make({ orgId, data: updated })]);\n }\n return updated;\n });\n this.logger.info('asset confirmed', { orgId, assetId: id });\n return updated;\n }\n\n list(orgId: string, query: AssetsQuery): Promise<Page<Asset>> {\n return this.repo.list(orgId, query);\n }\n\n get(orgId: string, id: string): Promise<Asset | null> {\n return this.repo.findById(orgId, id);\n }\n\n async requestDownload(\n orgId: string,\n id: string,\n filename?: string,\n expiresInSeconds?: number,\n ): Promise<DownloadTicket | null> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n return null;\n }\n const url = await this.storage.presignDownload({\n key: asset.key,\n downloadFilename: filename ?? asset.filename,\n ...(expiresInSeconds === undefined ? {} : { expiresInSeconds }),\n });\n return { url, asset };\n }\n\n async remove(orgId: string, id: string): Promise<boolean> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n return false;\n }\n await this.storage.remove(asset.key);\n const deleted = await this.uow.run(async ({ assets, outbox }) => {\n const deleted = await assets.delete(id);\n if (deleted) {\n await outbox.append([assetEvents.assetDeleted.make({ orgId, data: asset })]);\n }\n return deleted;\n });\n this.logger.info('asset removed', { orgId, assetId: id });\n return deleted;\n }\n}\n"],"mappings":"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"}
@@ -1,11 +1,19 @@
1
1
  import { DomainEvent } from "../types/shared.js";
2
2
  import { EmailTemplateId, EmailTemplateParams } from "../types/email-templates.js";
3
3
  import { AutomationAction } from "../types/schemas/automations.js";
4
+ import { WebhookSender } from "../types/automations.js";
4
5
  import "../types/index.js";
5
- import "../shared/ports.js";
6
+ import { CredentialStore } from "../shared/ports.js";
6
7
  import { Mailer, MailerLookups } from "../shared/mailer.js";
7
8
  import "./model.js";
9
+ import "./ports.js";
8
10
  //#region src/automations/actions.d.ts
11
+ interface ActionDeps {
12
+ mailer: Pick<Mailer, 'send'>;
13
+ lookups: MailerLookups;
14
+ credentials: Pick<CredentialStore, 'reveal'>;
15
+ webhooks: WebhookSender;
16
+ }
9
17
  interface SendEmailDerivation<K extends EmailTemplateId> {
10
18
  /** The only event type this template's params can be derived from. */
11
19
  trigger: string;
@@ -18,7 +26,7 @@ interface SendEmailDerivation<K extends EmailTemplateId> {
18
26
  type SendEmailDerivations = { [K in EmailTemplateId]?: SendEmailDerivation<K>; };
19
27
  declare const SEND_EMAIL_DERIVATIONS: SendEmailDerivations;
20
28
  /** Throws on any failure — the engine owns retry policy and failure bookkeeping. */
21
- declare function executeAction(action: AutomationAction, event: DomainEvent, mailer: Pick<Mailer, 'send'>, lookups: MailerLookups): Promise<void>;
29
+ declare function executeAction(action: AutomationAction, event: DomainEvent, deps: ActionDeps): Promise<void>;
22
30
  //#endregion
23
- export { SEND_EMAIL_DERIVATIONS, executeAction };
31
+ export { ActionDeps, SEND_EMAIL_DERIVATIONS, executeAction };
24
32
  //# sourceMappingURL=actions.d.ts.map
@@ -1,5 +1,7 @@
1
+ import { deliverWebhookInputSchema } from "../types/schemas/automations.js";
1
2
  import { entitlementEvents } from "../entitlements/events.js";
2
- import { ALL_EMAIL_TEMPLATE_IDS } from "./catalog.js";
3
+ import { ALL_EMAIL_TEMPLATE_IDS, DELIVER_WEBHOOK_ACTION, publicDataSchema } from "./catalog.js";
4
+ import { webhookHeaders } from "./webhook-signing.js";
3
5
  //#region src/automations/actions.ts
4
6
  const SEND_EMAIL_DERIVATIONS = {
5
7
  accessGranted: {
@@ -8,13 +10,15 @@ const SEND_EMAIL_DERIVATIONS = {
8
10
  const result = entitlementEvents.entitlementCreated.safeParse(event);
9
11
  if (!result.success) return;
10
12
  const entitlement = result.data.data;
13
+ if (!entitlement.contentId) return;
11
14
  const [to, content] = await Promise.all([lookups.orgUserEmail(entitlement.orgId, entitlement.orgUserId), lookups.contentInfo(entitlement.orgId, entitlement.contentId)]);
12
15
  if (!to || !content) return;
13
16
  return {
14
17
  to,
15
18
  params: {
16
19
  contentTitle: content.title,
17
- contentId: content.id
20
+ contentId: content.id,
21
+ contentType: content.type
18
22
  }
19
23
  };
20
24
  }
@@ -25,6 +29,7 @@ const SEND_EMAIL_DERIVATIONS = {
25
29
  const result = entitlementEvents.entitlementDeleted.safeParse(event);
26
30
  if (!result.success) return;
27
31
  const entitlement = result.data.data;
32
+ if (!entitlement.contentId) return;
28
33
  const [to, content] = await Promise.all([lookups.orgUserEmail(entitlement.orgId, entitlement.orgUserId), lookups.contentInfo(entitlement.orgId, entitlement.contentId)]);
29
34
  if (!to || !content) return;
30
35
  return {
@@ -38,21 +43,49 @@ function isEmailTemplateId(value) {
38
43
  return typeof value === "string" && value in ALL_EMAIL_TEMPLATE_IDS;
39
44
  }
40
45
  /** Throws on any failure — the engine owns retry policy and failure bookkeeping. */
41
- async function executeAction(action, event, mailer, lookups) {
46
+ async function executeAction(action, event, deps) {
42
47
  switch (action.type) {
43
48
  case "sendEmail": {
44
49
  const template = action.input["template"];
45
50
  if (!isEmailTemplateId(template)) throw new Error(`sendEmail: unknown template "${String(template)}"`);
46
51
  const derivation = SEND_EMAIL_DERIVATIONS[template];
47
52
  if (!derivation || derivation.trigger !== event.type) throw new Error(`sendEmail: template "${template}" cannot be derived from event "${event.type}"`);
48
- const derived = await derivation.derive(event, lookups);
53
+ const derived = await derivation.derive(event, deps.lookups);
49
54
  if (!derived) throw new Error(`sendEmail: event "${event.type}" is missing the data required to derive template "${template}"`);
50
- await mailer.send(derived.to, template, derived.params);
55
+ await deps.mailer.send(derived.to, template, derived.params);
51
56
  return;
52
57
  }
58
+ case DELIVER_WEBHOOK_ACTION:
59
+ await deliverWebhook(action, event, deps);
60
+ return;
53
61
  default: throw new Error(`unknown automation action type "${action.type}"`);
54
62
  }
55
63
  }
64
+ async function deliverWebhook(action, event, deps) {
65
+ const input = deliverWebhookInputSchema.safeParse(action.input);
66
+ if (!input.success) throw new Error(`${DELIVER_WEBHOOK_ACTION}: invalid input`);
67
+ const publicData = publicDataSchema(event.type);
68
+ if (!publicData) throw new Error(`${DELIVER_WEBHOOK_ACTION}: event "${event.type}" is not published`);
69
+ const data = publicData.safeParse(event.data);
70
+ if (!data.success) throw new Error(`${DELIVER_WEBHOOK_ACTION}: event "${event.type}" does not match its published shape`);
71
+ const secret = (await deps.credentials.reveal(event.orgId, input.data.secretRef))?.["secret"];
72
+ if (typeof secret !== "string") throw new Error(`${DELIVER_WEBHOOK_ACTION}: signing secret not found`);
73
+ const body = JSON.stringify({
74
+ id: event.id,
75
+ type: event.type,
76
+ version: event.version,
77
+ orgId: event.orgId,
78
+ occurredAt: event.occurredAt,
79
+ data: data.data
80
+ });
81
+ const timestamp = Math.floor(Date.now() / 1e3);
82
+ const response = await deps.webhooks.send({
83
+ url: input.data.url,
84
+ headers: webhookHeaders(secret, event.id, timestamp, body),
85
+ body
86
+ });
87
+ if (response.status < 200 || response.status >= 300) throw new Error(`${DELIVER_WEBHOOK_ACTION}: endpoint responded ${response.status}`);
88
+ }
56
89
  //#endregion
57
90
  export { SEND_EMAIL_DERIVATIONS, executeAction };
58
91
 
@@ -1 +1 @@
1
- {"version":3,"file":"actions.js","names":[],"sources":["../../src/automations/actions.ts"],"sourcesContent":["// automations context — action runners. `executeAction` maps one\n// AutomationAction against the DomainEvent that triggered its run.\n//\n// SEND_EMAIL_DERIVATIONS is the sendEmail runner's internal derivation table:\n// which (trigger, template) pairings are derivable. A template with no entry\n// (e.g. courseCompleted) makes `executeAction` throw a named error, recorded\n// by the engine as a failed action.\nimport type { EmailTemplateId, EmailTemplateParams } from '../types/index.js';\nimport { entitlementEvents } from '../entitlements/index.js';\nimport type { Mailer, MailerLookups } from '../shared/mailer.js';\nimport type { DomainEvent } from '../shared/ports.js';\nimport type { AutomationAction } from './model.js';\nimport { ALL_EMAIL_TEMPLATE_IDS } from './catalog.js';\n\ninterface SendEmailDerivation<K extends EmailTemplateId> {\n /** The only event type this template's params can be derived from. */\n trigger: string;\n /** Undefined = the recipient or content behind the event's ids cannot be resolved. */\n derive(\n event: DomainEvent,\n lookups: MailerLookups,\n ): Promise<{ to: string; params: EmailTemplateParams[K] } | undefined>;\n}\n\ntype SendEmailDerivations = { [K in EmailTemplateId]?: SendEmailDerivation<K> };\n\nexport const SEND_EMAIL_DERIVATIONS: SendEmailDerivations = {\n accessGranted: {\n trigger: 'entitlement.created',\n derive: async (event, lookups) => {\n const result = entitlementEvents.entitlementCreated.safeParse(event);\n if (!result.success) {\n return undefined;\n }\n const entitlement = result.data.data;\n const [to, content] = await Promise.all([\n lookups.orgUserEmail(entitlement.orgId, entitlement.orgUserId),\n lookups.contentInfo(entitlement.orgId, entitlement.contentId),\n ]);\n if (!to || !content) {\n return undefined;\n }\n return {\n to,\n params: { contentTitle: content.title, contentId: content.id },\n };\n },\n },\n accessRevoked: {\n trigger: 'entitlement.deleted',\n derive: async (event, lookups) => {\n const result = entitlementEvents.entitlementDeleted.safeParse(event);\n if (!result.success) {\n return undefined;\n }\n const entitlement = result.data.data;\n const [to, content] = await Promise.all([\n lookups.orgUserEmail(entitlement.orgId, entitlement.orgUserId),\n lookups.contentInfo(entitlement.orgId, entitlement.contentId),\n ]);\n if (!to || !content) {\n return undefined;\n }\n return {\n to,\n params: { contentTitle: content.title },\n };\n },\n },\n};\n\nfunction isEmailTemplateId(value: unknown): value is EmailTemplateId {\n return typeof value === 'string' && value in ALL_EMAIL_TEMPLATE_IDS;\n}\n\n/** Throws on any failure — the engine owns retry policy and failure bookkeeping. */\nexport async function executeAction(\n action: AutomationAction,\n event: DomainEvent,\n mailer: Pick<Mailer, 'send'>,\n lookups: MailerLookups,\n): Promise<void> {\n switch (action.type) {\n case 'sendEmail': {\n const template = action.input['template'];\n if (!isEmailTemplateId(template)) {\n throw new Error(`sendEmail: unknown template \"${String(template)}\"`);\n }\n const derivation = SEND_EMAIL_DERIVATIONS[template];\n if (!derivation || derivation.trigger !== event.type) {\n throw new Error(\n `sendEmail: template \"${template}\" cannot be derived from event \"${event.type}\"`,\n );\n }\n const derived = await derivation.derive(event, lookups);\n if (!derived) {\n throw new Error(\n `sendEmail: event \"${event.type}\" is missing the data required to derive template \"${template}\"`,\n );\n }\n await mailer.send(derived.to, template, derived.params);\n return;\n }\n default: {\n throw new Error(`unknown automation action type \"${action.type}\"`);\n }\n }\n}\n"],"mappings":";;;AA0BA,MAAa,yBAA+C;CAC1D,eAAe;EACb,SAAS;EACT,QAAQ,OAAO,OAAO,YAAY;GAChC,MAAM,SAAS,kBAAkB,mBAAmB,UAAU,KAAK;GACnE,IAAI,CAAC,OAAO,SACV;GAEF,MAAM,cAAc,OAAO,KAAK;GAChC,MAAM,CAAC,IAAI,WAAW,MAAM,QAAQ,IAAI,CACtC,QAAQ,aAAa,YAAY,OAAO,YAAY,SAAS,GAC7D,QAAQ,YAAY,YAAY,OAAO,YAAY,SAAS,CAC9D,CAAC;GACD,IAAI,CAAC,MAAM,CAAC,SACV;GAEF,OAAO;IACL;IACA,QAAQ;KAAE,cAAc,QAAQ;KAAO,WAAW,QAAQ;IAAG;GAC/D;EACF;CACF;CACA,eAAe;EACb,SAAS;EACT,QAAQ,OAAO,OAAO,YAAY;GAChC,MAAM,SAAS,kBAAkB,mBAAmB,UAAU,KAAK;GACnE,IAAI,CAAC,OAAO,SACV;GAEF,MAAM,cAAc,OAAO,KAAK;GAChC,MAAM,CAAC,IAAI,WAAW,MAAM,QAAQ,IAAI,CACtC,QAAQ,aAAa,YAAY,OAAO,YAAY,SAAS,GAC7D,QAAQ,YAAY,YAAY,OAAO,YAAY,SAAS,CAC9D,CAAC;GACD,IAAI,CAAC,MAAM,CAAC,SACV;GAEF,OAAO;IACL;IACA,QAAQ,EAAE,cAAc,QAAQ,MAAM;GACxC;EACF;CACF;AACF;AAEA,SAAS,kBAAkB,OAA0C;CACnE,OAAO,OAAO,UAAU,YAAY,SAAS;AAC/C;;AAGA,eAAsB,cACpB,QACA,OACA,QACA,SACe;CACf,QAAQ,OAAO,MAAf;EACE,KAAK,aAAa;GAChB,MAAM,WAAW,OAAO,MAAM;GAC9B,IAAI,CAAC,kBAAkB,QAAQ,GAC7B,MAAM,IAAI,MAAM,gCAAgC,OAAO,QAAQ,EAAE,EAAE;GAErE,MAAM,aAAa,uBAAuB;GAC1C,IAAI,CAAC,cAAc,WAAW,YAAY,MAAM,MAC9C,MAAM,IAAI,MACR,wBAAwB,SAAS,kCAAkC,MAAM,KAAK,EAChF;GAEF,MAAM,UAAU,MAAM,WAAW,OAAO,OAAO,OAAO;GACtD,IAAI,CAAC,SACH,MAAM,IAAI,MACR,qBAAqB,MAAM,KAAK,qDAAqD,SAAS,EAChG;GAEF,MAAM,OAAO,KAAK,QAAQ,IAAI,UAAU,QAAQ,MAAM;GACtD;EACF;EACA,SACE,MAAM,IAAI,MAAM,mCAAmC,OAAO,KAAK,EAAE;CAErE;AACF"}
1
+ {"version":3,"file":"actions.js","names":[],"sources":["../../src/automations/actions.ts"],"sourcesContent":["// automations context — action runners. `executeAction` maps one\n// AutomationAction against the DomainEvent that triggered its run.\n//\n// SEND_EMAIL_DERIVATIONS is the sendEmail runner's internal derivation table:\n// which (trigger, template) pairings are derivable. A template with no entry\n// (e.g. courseCompleted) makes `executeAction` throw a named error, recorded\n// by the engine as a failed action.\nimport type { EmailTemplateId, EmailTemplateParams } from '../types/index.js';\nimport { deliverWebhookInputSchema } from '../types/schemas/index.js';\nimport { entitlementEvents } from '../entitlements/index.js';\nimport type { Mailer, MailerLookups } from '../shared/mailer.js';\nimport type { CredentialStore, DomainEvent } from '../shared/ports.js';\nimport type { AutomationAction } from './model.js';\nimport type { WebhookSender } from './ports.js';\nimport { ALL_EMAIL_TEMPLATE_IDS, DELIVER_WEBHOOK_ACTION, publicDataSchema } from './catalog.js';\nimport { webhookHeaders } from './webhook-signing.js';\n\nexport interface ActionDeps {\n mailer: Pick<Mailer, 'send'>;\n lookups: MailerLookups;\n credentials: Pick<CredentialStore, 'reveal'>;\n webhooks: WebhookSender;\n}\n\ninterface SendEmailDerivation<K extends EmailTemplateId> {\n /** The only event type this template's params can be derived from. */\n trigger: string;\n /** Undefined = the recipient or content behind the event's ids cannot be resolved. */\n derive(\n event: DomainEvent,\n lookups: MailerLookups,\n ): Promise<{ to: string; params: EmailTemplateParams[K] } | undefined>;\n}\n\ntype SendEmailDerivations = { [K in EmailTemplateId]?: SendEmailDerivation<K> };\n\nexport const SEND_EMAIL_DERIVATIONS: SendEmailDerivations = {\n accessGranted: {\n trigger: 'entitlement.created',\n derive: async (event, lookups) => {\n const result = entitlementEvents.entitlementCreated.safeParse(event);\n if (!result.success) {\n return undefined;\n }\n const entitlement = result.data.data;\n // Bundle grants carry no content id — this email is per content item.\n if (!entitlement.contentId) {\n return undefined;\n }\n const [to, content] = await Promise.all([\n lookups.orgUserEmail(entitlement.orgId, entitlement.orgUserId),\n lookups.contentInfo(entitlement.orgId, entitlement.contentId),\n ]);\n if (!to || !content) {\n return undefined;\n }\n return {\n to,\n params: { contentTitle: content.title, contentId: content.id, contentType: content.type },\n };\n },\n },\n accessRevoked: {\n trigger: 'entitlement.deleted',\n derive: async (event, lookups) => {\n const result = entitlementEvents.entitlementDeleted.safeParse(event);\n if (!result.success) {\n return undefined;\n }\n const entitlement = result.data.data;\n // Bundle grants carry no content id — this email is per content item.\n if (!entitlement.contentId) {\n return undefined;\n }\n const [to, content] = await Promise.all([\n lookups.orgUserEmail(entitlement.orgId, entitlement.orgUserId),\n lookups.contentInfo(entitlement.orgId, entitlement.contentId),\n ]);\n if (!to || !content) {\n return undefined;\n }\n return {\n to,\n params: { contentTitle: content.title },\n };\n },\n },\n};\n\nfunction isEmailTemplateId(value: unknown): value is EmailTemplateId {\n return typeof value === 'string' && value in ALL_EMAIL_TEMPLATE_IDS;\n}\n\n/** Throws on any failure — the engine owns retry policy and failure bookkeeping. */\nexport async function executeAction(\n action: AutomationAction,\n event: DomainEvent,\n deps: ActionDeps,\n): Promise<void> {\n switch (action.type) {\n case 'sendEmail': {\n const template = action.input['template'];\n if (!isEmailTemplateId(template)) {\n throw new Error(`sendEmail: unknown template \"${String(template)}\"`);\n }\n const derivation = SEND_EMAIL_DERIVATIONS[template];\n if (!derivation || derivation.trigger !== event.type) {\n throw new Error(\n `sendEmail: template \"${template}\" cannot be derived from event \"${event.type}\"`,\n );\n }\n const derived = await derivation.derive(event, deps.lookups);\n if (!derived) {\n throw new Error(\n `sendEmail: event \"${event.type}\" is missing the data required to derive template \"${template}\"`,\n );\n }\n await deps.mailer.send(derived.to, template, derived.params);\n return;\n }\n case DELIVER_WEBHOOK_ACTION: {\n await deliverWebhook(action, event, deps);\n return;\n }\n default: {\n throw new Error(`unknown automation action type \"${action.type}\"`);\n }\n }\n}\n\nasync function deliverWebhook(\n action: AutomationAction,\n event: DomainEvent,\n deps: ActionDeps,\n): Promise<void> {\n const input = deliverWebhookInputSchema.safeParse(action.input);\n if (!input.success) {\n throw new Error(`${DELIVER_WEBHOOK_ACTION}: invalid input`);\n }\n const publicData = publicDataSchema(event.type);\n if (!publicData) {\n throw new Error(`${DELIVER_WEBHOOK_ACTION}: event \"${event.type}\" is not published`);\n }\n const data = publicData.safeParse(event.data);\n if (!data.success) {\n throw new Error(\n `${DELIVER_WEBHOOK_ACTION}: event \"${event.type}\" does not match its published shape`,\n );\n }\n const secrets = await deps.credentials.reveal(event.orgId, input.data.secretRef);\n const secret = secrets?.['secret'];\n if (typeof secret !== 'string') {\n throw new Error(`${DELIVER_WEBHOOK_ACTION}: signing secret not found`);\n }\n const body = JSON.stringify({\n id: event.id,\n type: event.type,\n version: event.version,\n orgId: event.orgId,\n occurredAt: event.occurredAt,\n data: data.data,\n });\n const timestamp = Math.floor(Date.now() / 1000);\n const response = await deps.webhooks.send({\n url: input.data.url,\n headers: webhookHeaders(secret, event.id, timestamp, body),\n body,\n });\n if (response.status < 200 || response.status >= 300) {\n throw new Error(`${DELIVER_WEBHOOK_ACTION}: endpoint responded ${response.status}`);\n }\n}\n"],"mappings":"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"}
@@ -2,11 +2,14 @@ import { EmailTemplateId } from "../types/email-templates.js";
2
2
  import { AvailableActions, AvailableTriggers } from "../types/schemas/automations.js";
3
3
  import "../types/index.js";
4
4
  import "./types.js";
5
+ import { z } from "zod";
5
6
  //#region src/automations/catalog.d.ts
6
7
  /** Every EmailTemplateId — a missing key here is a compile error, kept exhaustive by construction. */
7
8
  declare const ALL_EMAIL_TEMPLATE_IDS: Record<EmailTemplateId, true>;
9
+ declare const DELIVER_WEBHOOK_ACTION = "deliverWebhook";
8
10
  declare function catalogActions(): AvailableActions;
9
11
  declare function catalogTriggers(): AvailableTriggers['triggers'];
12
+ declare function publicDataSchema(type: string): z.ZodType | undefined;
10
13
  //#endregion
11
- export { ALL_EMAIL_TEMPLATE_IDS, catalogActions, catalogTriggers };
14
+ export { ALL_EMAIL_TEMPLATE_IDS, DELIVER_WEBHOOK_ACTION, catalogActions, catalogTriggers, publicDataSchema };
12
15
  //# sourceMappingURL=catalog.d.ts.map