@headless-lms/server 0.4.0 → 0.6.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 (222) hide show
  1. package/README.md +14 -0
  2. package/dist/app/community-mail.d.ts +5 -3
  3. package/dist/app/community-mail.js +60 -12
  4. package/dist/app/community-mail.js.map +1 -1
  5. package/dist/app/container.d.ts +24 -3
  6. package/dist/app/container.js +102 -38
  7. package/dist/app/container.js.map +1 -1
  8. package/dist/app/notification-channels.js +1 -0
  9. package/dist/app/notification-channels.js.map +1 -1
  10. package/dist/app/notification-reads.d.ts +3 -3
  11. package/dist/app/notification-reads.js +7 -28
  12. package/dist/app/notification-reads.js.map +1 -1
  13. package/dist/app/platform.d.ts +14 -0
  14. package/dist/app/platform.js +14 -0
  15. package/dist/app/platform.js.map +1 -0
  16. package/dist/http/config.d.ts +4 -0
  17. package/dist/http/errors.js +9 -4
  18. package/dist/http/errors.js.map +1 -1
  19. package/dist/http/fastify.d.ts +8 -0
  20. package/dist/http/mcp/catalogue.d.ts +11 -9
  21. package/dist/http/mcp/catalogue.js +15 -21
  22. package/dist/http/mcp/catalogue.js.map +1 -1
  23. package/dist/http/mcp/challenge.js +3 -3
  24. package/dist/http/mcp/challenge.js.map +1 -1
  25. package/dist/http/mcp/route.js +7 -7
  26. package/dist/http/mcp/route.js.map +1 -1
  27. package/dist/http/mcp/test-support.d.ts +6 -5
  28. package/dist/http/mcp/test-support.js +32 -11
  29. package/dist/http/mcp/test-support.js.map +1 -1
  30. package/dist/http/mcp/tool.d.ts +3 -9
  31. package/dist/http/mcp/tool.js +14 -6
  32. package/dist/http/mcp/tool.js.map +1 -1
  33. package/dist/http/mcp/tools/automations.js +10 -20
  34. package/dist/http/mcp/tools/automations.js.map +1 -1
  35. package/dist/http/mcp/tools/catalog.js +25 -42
  36. package/dist/http/mcp/tools/catalog.js.map +1 -1
  37. package/dist/http/mcp/tools/comments.js +9 -19
  38. package/dist/http/mcp/tools/comments.js.map +1 -1
  39. package/dist/http/mcp/tools/communities.js +17 -32
  40. package/dist/http/mcp/tools/communities.js.map +1 -1
  41. package/dist/http/mcp/tools/community-members.js +9 -16
  42. package/dist/http/mcp/tools/community-members.js.map +1 -1
  43. package/dist/http/mcp/tools/community-moderation.js +13 -25
  44. package/dist/http/mcp/tools/community-moderation.js.map +1 -1
  45. package/dist/http/mcp/tools/courses.js +11 -21
  46. package/dist/http/mcp/tools/courses.js.map +1 -1
  47. package/dist/http/mcp/tools/events.d.ts +6 -0
  48. package/dist/http/mcp/tools/events.js +170 -0
  49. package/dist/http/mcp/tools/events.js.map +1 -0
  50. package/dist/http/mcp/tools/index.d.ts +5 -1
  51. package/dist/http/mcp/tools/index.js +45 -10
  52. package/dist/http/mcp/tools/index.js.map +1 -1
  53. package/dist/http/mcp/tools/organization.d.ts +6 -0
  54. package/dist/http/mcp/tools/organization.js +214 -0
  55. package/dist/http/mcp/tools/organization.js.map +1 -0
  56. package/dist/http/mcp/tools/people.js +30 -49
  57. package/dist/http/mcp/tools/people.js.map +1 -1
  58. package/dist/http/plugins/auth.js +37 -8
  59. package/dist/http/plugins/auth.js.map +1 -1
  60. package/dist/http/plugins/error-handler.d.ts +5 -1
  61. package/dist/http/plugins/error-handler.js +10 -2
  62. package/dist/http/plugins/error-handler.js.map +1 -1
  63. package/dist/http/plugins/openapi.js +18 -2
  64. package/dist/http/plugins/openapi.js.map +1 -1
  65. package/dist/http/plugins/platform.d.ts +7 -0
  66. package/dist/http/plugins/platform.js +13 -0
  67. package/dist/http/plugins/platform.js.map +1 -0
  68. package/dist/http/routes/access.js +54 -36
  69. package/dist/http/routes/access.js.map +1 -1
  70. package/dist/http/routes/activities.js +45 -45
  71. package/dist/http/routes/activities.js.map +1 -1
  72. package/dist/http/routes/assets.js +30 -65
  73. package/dist/http/routes/assets.js.map +1 -1
  74. package/dist/http/routes/automations.js +37 -42
  75. package/dist/http/routes/automations.js.map +1 -1
  76. package/dist/http/routes/bundles.js +33 -36
  77. package/dist/http/routes/bundles.js.map +1 -1
  78. package/dist/http/routes/communities.js +93 -145
  79. package/dist/http/routes/communities.js.map +1 -1
  80. package/dist/http/routes/community-moderation.js +83 -137
  81. package/dist/http/routes/community-moderation.js.map +1 -1
  82. package/dist/http/routes/course-analytics.js +32 -15
  83. package/dist/http/routes/course-analytics.js.map +1 -1
  84. package/dist/http/routes/courses.js +25 -32
  85. package/dist/http/routes/courses.js.map +1 -1
  86. package/dist/http/routes/dashboard.js +9 -9
  87. package/dist/http/routes/dashboard.js.map +1 -1
  88. package/dist/http/routes/discussion.js +21 -39
  89. package/dist/http/routes/discussion.js.map +1 -1
  90. package/dist/http/routes/downloads.js +41 -47
  91. package/dist/http/routes/downloads.js.map +1 -1
  92. package/dist/http/routes/entitlements.js +19 -23
  93. package/dist/http/routes/entitlements.js.map +1 -1
  94. package/dist/http/routes/events.d.ts +7 -0
  95. package/dist/http/routes/events.js +333 -0
  96. package/dist/http/routes/events.js.map +1 -0
  97. package/dist/http/routes/integrations.js +40 -73
  98. package/dist/http/routes/integrations.js.map +1 -1
  99. package/dist/http/routes/member/assets.js +10 -13
  100. package/dist/http/routes/member/assets.js.map +1 -1
  101. package/dist/http/routes/member/chats.js +170 -47
  102. package/dist/http/routes/member/chats.js.map +1 -1
  103. package/dist/http/routes/member/comments.js +65 -26
  104. package/dist/http/routes/member/comments.js.map +1 -1
  105. package/dist/http/routes/member/community.js +45 -52
  106. package/dist/http/routes/member/community.js.map +1 -1
  107. package/dist/http/routes/member/courses.js +12 -19
  108. package/dist/http/routes/member/courses.js.map +1 -1
  109. package/dist/http/routes/member/devices.js +9 -10
  110. package/dist/http/routes/member/devices.js.map +1 -1
  111. package/dist/http/routes/member/downloads.js +6 -9
  112. package/dist/http/routes/member/downloads.js.map +1 -1
  113. package/dist/http/routes/member/events.d.ts +7 -0
  114. package/dist/http/routes/member/events.js +386 -0
  115. package/dist/http/routes/member/events.js.map +1 -0
  116. package/dist/http/routes/member/files.js +88 -46
  117. package/dist/http/routes/member/files.js.map +1 -1
  118. package/dist/http/routes/member/index.js +2 -0
  119. package/dist/http/routes/member/index.js.map +1 -1
  120. package/dist/http/routes/member/notification-settings.js +15 -23
  121. package/dist/http/routes/member/notification-settings.js.map +1 -1
  122. package/dist/http/routes/member/notifications.js +16 -16
  123. package/dist/http/routes/member/notifications.js.map +1 -1
  124. package/dist/http/routes/member/org.js +5 -15
  125. package/dist/http/routes/member/org.js.map +1 -1
  126. package/dist/http/routes/member/posts.js +67 -68
  127. package/dist/http/routes/member/posts.js.map +1 -1
  128. package/dist/http/routes/member/profile.js +15 -6
  129. package/dist/http/routes/member/profile.js.map +1 -1
  130. package/dist/http/routes/member/progress.js +7 -11
  131. package/dist/http/routes/member/progress.js.map +1 -1
  132. package/dist/http/routes/organizations.js +319 -135
  133. package/dist/http/routes/organizations.js.map +1 -1
  134. package/dist/http/routes/platform.d.ts +7 -0
  135. package/dist/http/routes/platform.js +61 -0
  136. package/dist/http/routes/platform.js.map +1 -0
  137. package/dist/http/routes/students.js +12 -75
  138. package/dist/http/routes/students.js.map +1 -1
  139. package/dist/http/routes/webhooks.js +30 -39
  140. package/dist/http/routes/webhooks.js.map +1 -1
  141. package/dist/http/routes.js +9 -0
  142. package/dist/http/routes.js.map +1 -1
  143. package/dist/http/schemas/access.d.ts +346 -64
  144. package/dist/http/schemas/access.js +39 -12
  145. package/dist/http/schemas/access.js.map +1 -1
  146. package/dist/http/schemas/assets.d.ts +8 -3
  147. package/dist/http/schemas/assets.js +6 -5
  148. package/dist/http/schemas/assets.js.map +1 -1
  149. package/dist/http/schemas/automations.d.ts +4 -4
  150. package/dist/http/schemas/chats.d.ts +216 -37
  151. package/dist/http/schemas/chats.js +13 -4
  152. package/dist/http/schemas/chats.js.map +1 -1
  153. package/dist/http/schemas/community.d.ts +105 -31
  154. package/dist/http/schemas/community.js +25 -9
  155. package/dist/http/schemas/community.js.map +1 -1
  156. package/dist/http/schemas/course-analytics.d.ts +51 -1
  157. package/dist/http/schemas/course-analytics.js +19 -1
  158. package/dist/http/schemas/course-analytics.js.map +1 -1
  159. package/dist/http/schemas/dashboard.d.ts +1 -0
  160. package/dist/http/schemas/dashboard.js +2 -1
  161. package/dist/http/schemas/dashboard.js.map +1 -1
  162. package/dist/http/schemas/discussion.d.ts +22 -72
  163. package/dist/http/schemas/discussion.js +2 -2
  164. package/dist/http/schemas/discussion.js.map +1 -1
  165. package/dist/http/schemas/entitlements.d.ts +9 -12
  166. package/dist/http/schemas/entitlements.js +11 -5
  167. package/dist/http/schemas/entitlements.js.map +1 -1
  168. package/dist/http/schemas/events.d.ts +527 -0
  169. package/dist/http/schemas/events.js +64 -0
  170. package/dist/http/schemas/events.js.map +1 -0
  171. package/dist/http/schemas/files.d.ts +24 -5
  172. package/dist/http/schemas/files.js +15 -2
  173. package/dist/http/schemas/files.js.map +1 -1
  174. package/dist/http/schemas/index.d.ts +11 -9
  175. package/dist/http/schemas/index.js +11 -9
  176. package/dist/http/schemas/invites.d.ts +3 -47
  177. package/dist/http/schemas/invites.js +4 -25
  178. package/dist/http/schemas/invites.js.map +1 -1
  179. package/dist/http/schemas/member.d.ts +94 -7
  180. package/dist/http/schemas/member.js +7 -3
  181. package/dist/http/schemas/member.js.map +1 -1
  182. package/dist/http/schemas/members.d.ts +329 -65
  183. package/dist/http/schemas/members.js +67 -25
  184. package/dist/http/schemas/members.js.map +1 -1
  185. package/dist/http/schemas/notifications.d.ts +58 -54
  186. package/dist/http/schemas/platform.d.ts +38 -0
  187. package/dist/http/schemas/platform.js +19 -0
  188. package/dist/http/schemas/platform.js.map +1 -0
  189. package/dist/http/schemas/posts.d.ts +55 -35
  190. package/dist/http/schemas/posts.js +4 -4
  191. package/dist/http/schemas/posts.js.map +1 -1
  192. package/dist/http/schemas/profile.d.ts +1 -1
  193. package/dist/http/schemas/shared.d.ts +1 -11
  194. package/dist/http/schemas/shared.js +1 -10
  195. package/dist/http/schemas/shared.js.map +1 -1
  196. package/dist/http/schemas/students.d.ts +22 -0
  197. package/dist/http/schemas/students.js +3 -1
  198. package/dist/http/schemas/students.js.map +1 -1
  199. package/dist/http/scope.d.ts +9 -13
  200. package/dist/http/scope.js +29 -40
  201. package/dist/http/scope.js.map +1 -1
  202. package/dist/http/server.js +9 -0
  203. package/dist/http/server.js.map +1 -1
  204. package/dist/http/shutdown.d.ts +11 -0
  205. package/dist/http/shutdown.js +40 -0
  206. package/dist/http/shutdown.js.map +1 -0
  207. package/dist/http/sse.d.ts +19 -0
  208. package/dist/http/sse.js +104 -0
  209. package/dist/http/sse.js.map +1 -0
  210. package/dist/http/test-caller.d.ts +13 -0
  211. package/dist/http/test-caller.js +14 -0
  212. package/dist/http/test-caller.js.map +1 -0
  213. package/dist/http/well-known.js +2 -2
  214. package/dist/http/well-known.js.map +1 -1
  215. package/dist/index.d.ts +4 -2
  216. package/dist/index.js +4 -2
  217. package/dist/index.js.map +1 -1
  218. package/dist/packages/server/package.js +1 -1
  219. package/package.json +7 -6
  220. package/dist/http/routes/member/actor.d.ts +0 -14
  221. package/dist/http/routes/member/actor.js +0 -11
  222. package/dist/http/routes/member/actor.js.map +0 -1
@@ -1,129 +1,98 @@
1
1
  import { refusals } from "../schemas/shared.js";
2
- import { resolveScope } from "../scope.js";
2
+ import { resolveActor } from "../scope.js";
3
3
  import { Asset, AssetIdParam, AssetsPage, AssetsQuery, RequestUpload, UploadTicket } from "../schemas/assets.js";
4
4
  import "../schemas/index.js";
5
5
  import { z } from "zod";
6
- import { NotFoundError } from "@headless-lms/core/shared/errors";
7
6
  //#region src/http/routes/assets.ts
8
- /** Resolve the session's active org to the domain org id, or 400 and return null. */
9
- async function resolveOrgId(req, reply) {
10
- if (!req.orgId) {
11
- await reply.code(400).send({
12
- error: "no_active_org",
13
- message: "No active organization"
14
- });
15
- return null;
16
- }
17
- return req.orgId;
18
- }
19
7
  async function assetsRoutes(app, container) {
20
8
  const r = app.withTypeProvider();
21
9
  const assets = container.assets;
22
10
  const tags = ["Assets"];
23
- const staff = async (req) => {
24
- await resolveScope(container, req);
25
- };
26
11
  r.route({
27
12
  method: "POST",
28
13
  url: "/api/uploads",
29
- preHandler: staff,
30
14
  schema: {
31
15
  operationId: "requestUpload",
32
16
  tags,
33
- summary: "Register a library asset and get a presigned upload URL (staff)",
34
- description: "Staff only (owner, admin, instructor). The asset stays pending until you upload the file and confirm it.",
17
+ summary: "Register a library asset and get a presigned upload URL",
18
+ description: `Requires one of \`media:write\`, \`courses:write\`, \`downloads:write\`, \`communities:write\`. \`media:write\` uploads to the library; the write permission of a content item uploads for that item when \`contentId\` names it, and the asset records it. The asset stays pending until you upload the file and confirm it. 404 when \`contentId\` is not a content item of the organization.`,
35
19
  body: RequestUpload,
36
20
  response: {
37
21
  201: UploadTicket,
38
- ...refusals(403)
22
+ ...refusals(403, 404)
39
23
  }
40
24
  },
41
25
  handler: async (req, reply) => {
42
- const scope = await resolveScope(container, req);
43
- const ticket = await assets.requestUpload(scope.orgId, {
44
- ...req.body,
45
- source: "library",
46
- uploadedBy: scope.orgUserId
47
- });
26
+ const actor = await resolveActor(container, req);
27
+ const ticket = await assets.requestUpload(req.body, actor);
48
28
  return reply.code(201).send(ticket);
49
29
  }
50
30
  });
51
31
  r.route({
52
32
  method: "POST",
53
33
  url: "/api/assets/:id/confirm",
54
- preHandler: staff,
55
34
  schema: {
56
35
  operationId: "confirmAsset",
57
36
  tags,
58
- summary: "Confirm a library upload completed (staff)",
59
- description: "Staff only (owner, admin, instructor). Captures the stored size and content type: a pending library upload becomes ready; a ready one is returned unchanged. A member upload is finalized only by its uploader and is answered with 403.",
37
+ summary: "Confirm a library upload completed",
38
+ description: `Requires one of \`media:read\`, \`courses:write\`, \`downloads:write\`, \`communities:write\`. \`media:read\` reaches every file; the write permission of a content item reaches the files uploaded for it. Captures the stored size and content type: a pending library upload becomes ready; a ready one is returned unchanged. A member upload is finalized only by its uploader and is answered with 403.`,
60
39
  params: AssetIdParam,
61
40
  response: {
62
41
  200: Asset,
63
42
  ...refusals(403, 404)
64
43
  }
65
44
  },
66
- handler: async (req, reply) => {
67
- const orgId = await resolveOrgId(req, reply);
68
- if (!orgId) return;
69
- const asset = await assets.confirm(orgId, req.params.id);
70
- if (!asset) throw new NotFoundError("Asset", req.params.id);
71
- return asset;
45
+ handler: async (req) => {
46
+ const actor = await resolveActor(container, req);
47
+ return assets.confirm(req.params.id, actor);
72
48
  }
73
49
  });
74
50
  r.route({
75
51
  method: "GET",
76
52
  url: "/api/assets",
77
- preHandler: staff,
78
53
  schema: {
79
54
  operationId: "listAssets",
80
55
  tags,
81
- summary: "Browse the organization's media library (staff)",
82
- description: "Staff only (owner, admin, instructor). Library files by default; `source: member` lists members' uploads.",
56
+ summary: "Browse the organization's media library",
57
+ description: `Requires one of \`media:read\`, \`courses:write\`, \`downloads:write\`, \`communities:write\`. \`media:read\` lists the whole library, \`source: member\` members' uploads; the write permission of a content item lists the files uploaded for it, named by \`contentId\`.`,
83
58
  querystring: AssetsQuery,
84
59
  response: {
85
60
  200: AssetsPage,
86
61
  ...refusals(403)
87
62
  }
88
63
  },
89
- handler: async (req, reply) => {
90
- const orgId = await resolveOrgId(req, reply);
91
- if (!orgId) return;
92
- return assets.list(orgId, req.query);
64
+ handler: async (req) => {
65
+ const actor = await resolveActor(container, req);
66
+ return assets.list(req.query, actor);
93
67
  }
94
68
  });
95
69
  r.route({
96
70
  method: "GET",
97
71
  url: "/api/assets/:id",
98
- preHandler: staff,
99
72
  schema: {
100
73
  operationId: "getAsset",
101
74
  tags,
102
- summary: "Get an asset's metadata (staff)",
103
- description: "Staff only (owner, admin, instructor).",
75
+ summary: "Get an asset's metadata",
76
+ description: `Requires one of \`media:read\`, \`courses:write\`, \`downloads:write\`, \`communities:write\`. \`media:read\` reaches every file; the write permission of a content item reaches the files uploaded for it.`,
104
77
  params: AssetIdParam,
105
78
  response: {
106
79
  200: Asset,
107
80
  ...refusals(403, 404)
108
81
  }
109
82
  },
110
- handler: async (req, reply) => {
111
- const orgId = await resolveOrgId(req, reply);
112
- if (!orgId) return;
113
- const asset = await assets.get(orgId, req.params.id);
114
- if (!asset) throw new NotFoundError("Asset", req.params.id);
115
- return asset;
83
+ handler: async (req) => {
84
+ const actor = await resolveActor(container, req);
85
+ return assets.get(req.params.id, actor);
116
86
  }
117
87
  });
118
88
  r.route({
119
89
  method: "DELETE",
120
90
  url: "/api/assets/:id",
121
- preHandler: staff,
122
91
  schema: {
123
92
  operationId: "deleteAsset",
124
93
  tags,
125
- summary: "Delete an asset nothing references (staff)",
126
- description: "Staff only (owner, admin, instructor). Removes the row and the stored file. 409 while content still uses it.",
94
+ summary: "Delete an asset nothing references",
95
+ description: `Requires \`media:delete\`. Removes the row and the stored file. 409 while content still uses it.`,
127
96
  params: AssetIdParam,
128
97
  response: {
129
98
  204: z.void(),
@@ -131,9 +100,8 @@ async function assetsRoutes(app, container) {
131
100
  }
132
101
  },
133
102
  handler: async (req, reply) => {
134
- const orgId = await resolveOrgId(req, reply);
135
- if (!orgId) return;
136
- if (!await assets.remove(orgId, req.params.id)) throw new NotFoundError("Asset", req.params.id);
103
+ const actor = await resolveActor(container, req);
104
+ await assets.remove(req.params.id, actor);
137
105
  return reply.code(204).send();
138
106
  }
139
107
  });
@@ -141,21 +109,20 @@ async function assetsRoutes(app, container) {
141
109
  suffix: "file",
142
110
  download: false,
143
111
  operationId: "getLibraryFile",
144
- summary: "Redirect to a library file to show it (staff)"
112
+ summary: "Redirect to a library file to show it"
145
113
  }, {
146
114
  suffix: "download",
147
115
  download: true,
148
116
  operationId: "downloadLibraryFile",
149
- summary: "Redirect to a library file saved under its filename (staff)"
117
+ summary: "Redirect to a library file saved under its filename"
150
118
  }]) r.route({
151
119
  method: "GET",
152
120
  url: `/api/assets/:id/${suffix}`,
153
- preHandler: staff,
154
121
  schema: {
155
122
  operationId,
156
123
  tags,
157
124
  summary,
158
- description: "Staff only (owner, admin, instructor). Ready library files of the organization; member uploads and pending files answer 404.",
125
+ description: `Requires one of \`media:read\`, \`courses:write\`, \`downloads:write\`, \`communities:write\`. \`media:read\` reaches every file; the write permission of a content item reaches the files uploaded for it. Ready library files of the organization; member uploads and pending files answer 404.`,
159
126
  params: AssetIdParam,
160
127
  response: {
161
128
  302: z.void(),
@@ -163,10 +130,8 @@ async function assetsRoutes(app, container) {
163
130
  }
164
131
  },
165
132
  handler: async (req, reply) => {
166
- const orgId = await resolveOrgId(req, reply);
167
- if (!orgId) return;
168
- const url = await assets.libraryFile(orgId, req.params.id, { download });
169
- if (!url) throw new NotFoundError("Asset", req.params.id);
133
+ const actor = await resolveActor(container, req);
134
+ const url = await assets.libraryFile(req.params.id, { download }, actor);
170
135
  return reply.header("cache-control", "no-store").header("referrer-policy", "no-referrer").redirect(url, 302);
171
136
  }
172
137
  });
@@ -1 +1 @@
1
- {"version":3,"file":"assets.js","names":[],"sources":["../../../src/http/routes/assets.ts"],"sourcesContent":["// HTTP routes for the assets media library. All require a session with an\n// active organization; assets are stored under the org's private prefix and\n// served through stable staff URLs that redirect to a signed storage URL.\n//\n// `req.orgId` is already the domain organization id these tables key on — the\n// session guard translates it from better-auth's id once per request.\nimport type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport {\n Asset,\n AssetIdParam,\n AssetsPage,\n AssetsQuery,\n RequestUpload,\n UploadTicket,\n refusals,\n} from '../schemas/index.js';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport type { Container } from '../../app/container.js';\nimport { resolveScope } from '../scope.js';\n\n/** Resolve the session's active org to the domain org id, or 400 and return null. */\nasync function resolveOrgId(req: FastifyRequest, reply: FastifyReply): Promise<string | null> {\n if (!req.orgId) {\n await reply.code(400).send({ error: 'no_active_org', message: 'No active organization' });\n return null;\n }\n return req.orgId;\n}\n\nexport async function assetsRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const assets = container.assets;\n const tags = ['Assets'];\n const staff = async (req: FastifyRequest) => {\n await resolveScope(container, req);\n };\n\n // Register an asset + get a presigned upload URL. Kept at /api/uploads as the\n // \"start an upload\" action; the resulting asset lives in the library below.\n r.route({\n method: 'POST',\n url: '/api/uploads',\n preHandler: staff,\n schema: {\n operationId: 'requestUpload',\n tags,\n summary: 'Register a library asset and get a presigned upload URL (staff)',\n description:\n 'Staff only (owner, admin, instructor). The asset stays pending until you upload the file and confirm it.',\n body: RequestUpload,\n response: { 201: UploadTicket, ...refusals(403) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const ticket = await assets.requestUpload(scope.orgId, {\n ...req.body,\n source: 'library',\n uploadedBy: scope.orgUserId,\n });\n return reply.code(201).send(ticket);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/assets/:id/confirm',\n preHandler: staff,\n schema: {\n operationId: 'confirmAsset',\n tags,\n summary: 'Confirm a library upload completed (staff)',\n description:\n 'Staff only (owner, admin, instructor). Captures the stored size and content type: a pending library upload becomes ready; a ready one is returned unchanged. A member upload is finalized only by its uploader and is answered with 403.',\n params: AssetIdParam,\n response: { 200: Asset, ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const asset = await assets.confirm(orgId, req.params.id);\n if (!asset) {\n throw new NotFoundError('Asset', req.params.id);\n }\n return asset;\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/assets',\n preHandler: staff,\n schema: {\n operationId: 'listAssets',\n tags,\n summary: \"Browse the organization's media library (staff)\",\n description:\n \"Staff only (owner, admin, instructor). Library files by default; `source: member` lists members' uploads.\",\n querystring: AssetsQuery,\n response: { 200: AssetsPage, ...refusals(403) },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n return assets.list(orgId, req.query);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/assets/:id',\n preHandler: staff,\n schema: {\n operationId: 'getAsset',\n tags,\n summary: \"Get an asset's metadata (staff)\",\n description: 'Staff only (owner, admin, instructor).',\n params: AssetIdParam,\n response: { 200: Asset, ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const asset = await assets.get(orgId, req.params.id);\n if (!asset) {\n throw new NotFoundError('Asset', req.params.id);\n }\n return asset;\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/assets/:id',\n preHandler: staff,\n schema: {\n operationId: 'deleteAsset',\n tags,\n summary: 'Delete an asset nothing references (staff)',\n description:\n 'Staff only (owner, admin, instructor). Removes the row and the stored file. 409 while content still uses it.',\n params: AssetIdParam,\n response: { 204: z.void(), ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const removed = await assets.remove(orgId, req.params.id);\n if (!removed) {\n throw new NotFoundError('Asset', req.params.id);\n }\n return reply.code(204).send();\n },\n });\n\n for (const { suffix, download, operationId, summary } of [\n {\n suffix: 'file',\n download: false,\n operationId: 'getLibraryFile',\n summary: 'Redirect to a library file to show it (staff)',\n },\n {\n suffix: 'download',\n download: true,\n operationId: 'downloadLibraryFile',\n summary: 'Redirect to a library file saved under its filename (staff)',\n },\n ]) {\n r.route({\n method: 'GET',\n url: `/api/assets/:id/${suffix}`,\n preHandler: staff,\n schema: {\n operationId,\n tags,\n summary,\n description:\n 'Staff only (owner, admin, instructor). Ready library files of the organization; member uploads and pending files answer 404.',\n params: AssetIdParam,\n response: { 302: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const orgId = await resolveOrgId(req, reply);\n if (!orgId) {\n return;\n }\n const url = await assets.libraryFile(orgId, req.params.id, { download });\n if (!url) {\n throw new NotFoundError('Asset', req.params.id);\n }\n return reply\n .header('cache-control', 'no-store')\n .header('referrer-policy', 'no-referrer')\n .redirect(url, 302);\n },\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"assets.js","names":[],"sources":["../../../src/http/routes/assets.ts"],"sourcesContent":["// HTTP routes for the assets media library. Assets are stored under the org's\n// private prefix and served through stable URLs that redirect to a signed\n// storage URL.\nimport type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport {\n Asset,\n AssetIdParam,\n AssetsPage,\n AssetsQuery,\n RequestUpload,\n UploadTicket,\n refusals,\n} from '../schemas/index.js';\nimport type { Container } from '../../app/container.js';\nimport { resolveActor } from '../scope.js';\n\nexport async function assetsRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const assets = container.assets;\n const tags = ['Assets'];\n\n r.route({\n method: 'POST',\n url: '/api/uploads',\n schema: {\n operationId: 'requestUpload',\n tags,\n summary: 'Register a library asset and get a presigned upload URL',\n description: `Requires one of \\`media:write\\`, \\`courses:write\\`, \\`downloads:write\\`, \\`communities:write\\`. \\`media:write\\` uploads to the library; the write permission of a content item uploads for that item when \\`contentId\\` names it, and the asset records it. The asset stays pending until you upload the file and confirm it. 404 when \\`contentId\\` is not a content item of the organization.`,\n body: RequestUpload,\n response: { 201: UploadTicket, ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const ticket = await assets.requestUpload(req.body, actor);\n return reply.code(201).send(ticket);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/assets/:id/confirm',\n schema: {\n operationId: 'confirmAsset',\n tags,\n summary: 'Confirm a library upload completed',\n description: `Requires one of \\`media:read\\`, \\`courses:write\\`, \\`downloads:write\\`, \\`communities:write\\`. \\`media:read\\` reaches every file; the write permission of a content item reaches the files uploaded for it. Captures the stored size and content type: a pending library upload becomes ready; a ready one is returned unchanged. A member upload is finalized only by its uploader and is answered with 403.`,\n params: AssetIdParam,\n response: { 200: Asset, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return assets.confirm(req.params.id, actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/assets',\n schema: {\n operationId: 'listAssets',\n tags,\n summary: \"Browse the organization's media library\",\n description: `Requires one of \\`media:read\\`, \\`courses:write\\`, \\`downloads:write\\`, \\`communities:write\\`. \\`media:read\\` lists the whole library, \\`source: member\\` members' uploads; the write permission of a content item lists the files uploaded for it, named by \\`contentId\\`.`,\n querystring: AssetsQuery,\n response: { 200: AssetsPage, ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return assets.list(req.query, actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/assets/:id',\n schema: {\n operationId: 'getAsset',\n tags,\n summary: \"Get an asset's metadata\",\n description: `Requires one of \\`media:read\\`, \\`courses:write\\`, \\`downloads:write\\`, \\`communities:write\\`. \\`media:read\\` reaches every file; the write permission of a content item reaches the files uploaded for it.`,\n params: AssetIdParam,\n response: { 200: Asset, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return assets.get(req.params.id, actor);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/assets/:id',\n schema: {\n operationId: 'deleteAsset',\n tags,\n summary: 'Delete an asset nothing references',\n description: `Requires \\`media:delete\\`. Removes the row and the stored file. 409 while content still uses it.`,\n params: AssetIdParam,\n response: { 204: z.void(), ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await assets.remove(req.params.id, actor);\n return reply.code(204).send();\n },\n });\n\n for (const { suffix, download, operationId, summary } of [\n {\n suffix: 'file',\n download: false,\n operationId: 'getLibraryFile',\n summary: 'Redirect to a library file to show it',\n },\n {\n suffix: 'download',\n download: true,\n operationId: 'downloadLibraryFile',\n summary: 'Redirect to a library file saved under its filename',\n },\n ]) {\n r.route({\n method: 'GET',\n url: `/api/assets/:id/${suffix}`,\n schema: {\n operationId,\n tags,\n summary,\n description: `Requires one of \\`media:read\\`, \\`courses:write\\`, \\`downloads:write\\`, \\`communities:write\\`. \\`media:read\\` reaches every file; the write permission of a content item reaches the files uploaded for it. Ready library files of the organization; member uploads and pending files answer 404.`,\n params: AssetIdParam,\n response: { 302: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const url = await assets.libraryFile(req.params.id, { download }, actor);\n return reply\n .header('cache-control', 'no-store')\n .header('referrer-policy', 'no-referrer')\n .redirect(url, 302);\n },\n });\n }\n}\n"],"mappings":"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"}
@@ -1,9 +1,8 @@
1
1
  import { refusals } from "../schemas/shared.js";
2
- import { resolveScope } from "../scope.js";
2
+ import { resolveActor } from "../scope.js";
3
3
  import { Automation, AutomationIdParam, AutomationRun, AutomationRunParams, AutomationRunsPage, AutomationRunsQuery, AutomationsQuery, AvailableAction, AvailableTriggers, CreateAutomationBody, UpdateAutomationBody } from "../schemas/automations.js";
4
4
  import "../schemas/index.js";
5
5
  import { z } from "zod";
6
- import { NotFoundError } from "@headless-lms/core/shared/errors";
7
6
  //#region src/http/routes/automations.ts
8
7
  async function automationsRoutes(app, container) {
9
8
  const r = app.withTypeProvider();
@@ -16,8 +15,8 @@ async function automationsRoutes(app, container) {
16
15
  schema: {
17
16
  operationId: "listAutomations",
18
17
  tags,
19
- summary: "List automations (staff)",
20
- description: "Staff only (owner, admin, instructor). Optionally only workflows or only webhooks.",
18
+ summary: "List automations",
19
+ description: `Requires \`automations:read\`. Optionally only workflows or only webhooks.`,
21
20
  querystring: AutomationsQuery,
22
21
  response: {
23
22
  200: z.array(Automation),
@@ -25,8 +24,8 @@ async function automationsRoutes(app, container) {
25
24
  }
26
25
  },
27
26
  handler: async (req) => {
28
- const scope = await resolveScope(container, req);
29
- return automations.list(scope.orgId, req.query);
27
+ const actor = await resolveActor(container, req);
28
+ return automations.list(req.query, actor);
30
29
  }
31
30
  });
32
31
  r.route({
@@ -36,16 +35,16 @@ async function automationsRoutes(app, container) {
36
35
  schema: {
37
36
  operationId: "listAutomationActions",
38
37
  tags,
39
- summary: "List the actions automations can use (staff)",
40
- description: "Staff only (owner, admin, instructor).",
38
+ summary: "List the actions automations can use",
39
+ description: "Requires `automations:read`.",
41
40
  response: {
42
41
  200: z.array(AvailableAction),
43
42
  ...refusals(403)
44
43
  }
45
44
  },
46
45
  handler: async (req) => {
47
- await resolveScope(container, req);
48
- return automations.availableActions();
46
+ const actor = await resolveActor(container, req);
47
+ return automations.availableActions(actor);
49
48
  }
50
49
  });
51
50
  r.route({
@@ -55,16 +54,16 @@ async function automationsRoutes(app, container) {
55
54
  schema: {
56
55
  operationId: "listAutomationTriggers",
57
56
  tags,
58
- summary: "List the domain events automations can react to (staff)",
59
- description: "Staff only (owner, admin, instructor).",
57
+ summary: "List the domain events automations can react to",
58
+ description: "Requires `automations:read`.",
60
59
  response: {
61
60
  200: AvailableTriggers,
62
61
  ...refusals(403)
63
62
  }
64
63
  },
65
64
  handler: async (req) => {
66
- await resolveScope(container, req);
67
- return automations.availableTriggers();
65
+ const actor = await resolveActor(container, req);
66
+ return automations.availableTriggers(actor);
68
67
  }
69
68
  });
70
69
  r.route({
@@ -74,8 +73,8 @@ async function automationsRoutes(app, container) {
74
73
  schema: {
75
74
  operationId: "createAutomation",
76
75
  tags,
77
- summary: "Create a workflow automation (staff)",
78
- description: "Staff only (owner, admin, instructor). 400 when a trigger is reserved, not an event the system emits or an unsupported schedule, or an action is reserved for webhooks or no longer available.",
76
+ summary: "Create a workflow automation",
77
+ description: `Requires \`automations:write\`. 400 when a trigger is reserved, not an event the system emits or an unsupported schedule, or an action is reserved for webhooks or no longer available.`,
79
78
  body: CreateAutomationBody,
80
79
  response: {
81
80
  201: Automation,
@@ -83,8 +82,8 @@ async function automationsRoutes(app, container) {
83
82
  }
84
83
  },
85
84
  handler: async (req, reply) => {
86
- const scope = await resolveScope(container, req);
87
- const automation = await automations.create(scope.orgId, req.body);
85
+ const actor = await resolveActor(container, req);
86
+ const automation = await automations.create(req.body, actor);
88
87
  return reply.code(201).send(automation);
89
88
  }
90
89
  });
@@ -95,8 +94,8 @@ async function automationsRoutes(app, container) {
95
94
  schema: {
96
95
  operationId: "getAutomation",
97
96
  tags,
98
- summary: "Get an automation (staff)",
99
- description: "Staff only (owner, admin, instructor). Workflows and webhooks alike.",
97
+ summary: "Get an automation",
98
+ description: `Requires \`automations:read\`. Workflows and webhooks alike.`,
100
99
  params: AutomationIdParam,
101
100
  response: {
102
101
  200: Automation,
@@ -104,10 +103,8 @@ async function automationsRoutes(app, container) {
104
103
  }
105
104
  },
106
105
  handler: async (req) => {
107
- const scope = await resolveScope(container, req);
108
- const automation = await automations.get(scope.orgId, req.params.id);
109
- if (!automation) throw new NotFoundError("Automation", req.params.id);
110
- return automation;
106
+ const actor = await resolveActor(container, req);
107
+ return automations.get(req.params.id, actor);
111
108
  }
112
109
  });
113
110
  r.route({
@@ -117,8 +114,8 @@ async function automationsRoutes(app, container) {
117
114
  schema: {
118
115
  operationId: "updateAutomation",
119
116
  tags,
120
- summary: "Update a workflow automation (staff)",
121
- description: "Staff only (owner, admin, instructor). Webhooks change through the webhook endpoints (409). 400 when a trigger is reserved, not an event the system emits or an unsupported schedule, or an action is reserved for webhooks or no longer available.",
117
+ summary: "Update a workflow automation",
118
+ description: `Requires \`automations:write\`. Webhooks change through the webhook endpoints (409). 400 when a trigger is reserved, not an event the system emits or an unsupported schedule, or an action is reserved for webhooks or no longer available.`,
122
119
  params: AutomationIdParam,
123
120
  body: UpdateAutomationBody,
124
121
  response: {
@@ -127,10 +124,8 @@ async function automationsRoutes(app, container) {
127
124
  }
128
125
  },
129
126
  handler: async (req) => {
130
- const scope = await resolveScope(container, req);
131
- const automation = await automations.update(scope.orgId, req.params.id, req.body);
132
- if (!automation) throw new NotFoundError("Automation", req.params.id);
133
- return automation;
127
+ const actor = await resolveActor(container, req);
128
+ return automations.update(req.params.id, req.body, actor);
134
129
  }
135
130
  });
136
131
  r.route({
@@ -140,8 +135,8 @@ async function automationsRoutes(app, container) {
140
135
  schema: {
141
136
  operationId: "deleteAutomation",
142
137
  tags,
143
- summary: "Delete a workflow automation (staff)",
144
- description: "Staff only (owner, admin, instructor). Webhooks are deleted through the webhook endpoints (409). Its runs stay readable.",
138
+ summary: "Delete a workflow automation",
139
+ description: `Requires \`automations:delete\`. Webhooks are deleted through the webhook endpoints (409). Its runs stay readable.`,
145
140
  params: AutomationIdParam,
146
141
  response: {
147
142
  204: z.void(),
@@ -149,8 +144,8 @@ async function automationsRoutes(app, container) {
149
144
  }
150
145
  },
151
146
  handler: async (req, reply) => {
152
- const scope = await resolveScope(container, req);
153
- if (!await automations.delete(scope.orgId, req.params.id)) throw new NotFoundError("Automation", req.params.id);
147
+ const actor = await resolveActor(container, req);
148
+ await automations.delete(req.params.id, actor);
154
149
  return reply.code(204).send();
155
150
  }
156
151
  });
@@ -161,8 +156,8 @@ async function automationsRoutes(app, container) {
161
156
  schema: {
162
157
  operationId: "listAutomationRuns",
163
158
  tags,
164
- summary: "List an automation's runs (staff)",
165
- description: "Staff only (owner, admin, instructor). A deleted automation's runs remain reachable as an audit trail, so an unknown id answers an empty page rather than 404. A run's event carries the fields a webhook delivery of that event would.",
159
+ summary: "List an automation's runs",
160
+ description: `Requires \`automations:read\`. A deleted automation's runs remain reachable as an audit trail, so an unknown id answers an empty page rather than 404. A run's event carries the fields a webhook delivery of that event would.`,
166
161
  params: AutomationIdParam,
167
162
  querystring: AutomationRunsQuery,
168
163
  response: {
@@ -171,8 +166,8 @@ async function automationsRoutes(app, container) {
171
166
  }
172
167
  },
173
168
  handler: async (req) => {
174
- const scope = await resolveScope(container, req);
175
- return automations.listRuns(scope.orgId, req.params.id, req.query);
169
+ const actor = await resolveActor(container, req);
170
+ return automations.listRuns(req.params.id, req.query, actor);
176
171
  }
177
172
  });
178
173
  r.route({
@@ -182,8 +177,8 @@ async function automationsRoutes(app, container) {
182
177
  schema: {
183
178
  operationId: "rerunAutomationRun",
184
179
  tags,
185
- summary: "Run a finished run again (staff)",
186
- description: "Staff only (owner, admin, instructor). Dispatches the same run again with the automation's current steps; the new outcome replaces the run's previous one. For a webhook this resends the delivery to its current URL. A run that is still running can't be rerun (409).",
180
+ summary: "Run a finished run again",
181
+ description: `Requires \`automations:write\`. Dispatches the same run again with the automation's current steps; the new outcome replaces the run's previous one. For a webhook this resends the delivery to its current URL. A run that is still running can't be rerun (409).`,
187
182
  params: AutomationRunParams,
188
183
  response: {
189
184
  200: AutomationRun,
@@ -191,8 +186,8 @@ async function automationsRoutes(app, container) {
191
186
  }
192
187
  },
193
188
  handler: async (req) => {
194
- const scope = await resolveScope(container, req);
195
- return automations.rerun(scope.orgId, req.params.id, req.params.runId);
189
+ const actor = await resolveActor(container, req);
190
+ return automations.rerun(req.params.id, req.params.runId, actor);
196
191
  }
197
192
  });
198
193
  }
@@ -1 +1 @@
1
- {"version":3,"file":"automations.js","names":[],"sources":["../../../src/http/routes/automations.ts"],"sourcesContent":["// HTTP routes for the automations context: rules that match a trigger (a\n// domain event type) against enabled automations and run an ordered list of\n// actions, plus the run history each automation accumulates.\nimport type { FastifyInstance } from 'fastify';\nimport { z } from 'zod';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n Automation,\n AutomationIdParam,\n AutomationRun,\n AutomationRunParams,\n AutomationRunsPage,\n AutomationRunsQuery,\n AutomationsQuery,\n AvailableAction,\n AvailableTriggers,\n CreateAutomationBody,\n UpdateAutomationBody,\n refusals,\n} from '../schemas/index.js';\nimport { NotFoundError } from '@headless-lms/core/shared/errors';\nimport type { Container } from '../../app/container.js';\nimport { resolveScope } from '../scope.js';\n\nexport async function automationsRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const automations = container.automations;\n const tags = ['Automations'];\n\n r.route({\n method: 'GET',\n url: '/api/automations',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listAutomations',\n tags,\n summary: 'List automations (staff)',\n description:\n 'Staff only (owner, admin, instructor). Optionally only workflows or only webhooks.',\n querystring: AutomationsQuery,\n response: { 200: z.array(Automation), ...refusals(403) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return automations.list(scope.orgId, req.query);\n },\n });\n\n // Before `/api/automations/:id` — otherwise `:id` would swallow these path segments.\n r.route({\n method: 'GET',\n url: '/api/automations/actions',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listAutomationActions',\n tags,\n summary: 'List the actions automations can use (staff)',\n description: 'Staff only (owner, admin, instructor).',\n response: { 200: z.array(AvailableAction), ...refusals(403) },\n },\n handler: async (req) => {\n await resolveScope(container, req);\n return automations.availableActions();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/automations/triggers',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listAutomationTriggers',\n tags,\n summary: 'List the domain events automations can react to (staff)',\n description: 'Staff only (owner, admin, instructor).',\n response: { 200: AvailableTriggers, ...refusals(403) },\n },\n handler: async (req) => {\n await resolveScope(container, req);\n return automations.availableTriggers();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/automations',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createAutomation',\n tags,\n summary: 'Create a workflow automation (staff)',\n description:\n 'Staff only (owner, admin, instructor). 400 when a trigger is reserved, not an event the system emits or an unsupported schedule, or an action is reserved for webhooks or no longer available.',\n body: CreateAutomationBody,\n response: { 201: Automation, ...refusals(400, 403) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const automation = await automations.create(scope.orgId, req.body);\n return reply.code(201).send(automation);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/automations/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getAutomation',\n tags,\n summary: 'Get an automation (staff)',\n description: 'Staff only (owner, admin, instructor). Workflows and webhooks alike.',\n params: AutomationIdParam,\n response: { 200: Automation, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const automation = await automations.get(scope.orgId, req.params.id);\n if (!automation) {\n throw new NotFoundError('Automation', req.params.id);\n }\n return automation;\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/automations/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateAutomation',\n tags,\n summary: 'Update a workflow automation (staff)',\n description:\n 'Staff only (owner, admin, instructor). Webhooks change through the webhook endpoints (409). 400 when a trigger is reserved, not an event the system emits or an unsupported schedule, or an action is reserved for webhooks or no longer available.',\n params: AutomationIdParam,\n body: UpdateAutomationBody,\n response: { 200: Automation, ...refusals(400, 403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n const automation = await automations.update(scope.orgId, req.params.id, req.body);\n if (!automation) {\n throw new NotFoundError('Automation', req.params.id);\n }\n return automation;\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/automations/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteAutomation',\n tags,\n summary: 'Delete a workflow automation (staff)',\n description:\n 'Staff only (owner, admin, instructor). Webhooks are deleted through the webhook endpoints (409). Its runs stay readable.',\n params: AutomationIdParam,\n response: { 204: z.void(), ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const scope = await resolveScope(container, req);\n const deleted = await automations.delete(scope.orgId, req.params.id);\n if (!deleted) {\n throw new NotFoundError('Automation', req.params.id);\n }\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/automations/:id/runs',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listAutomationRuns',\n tags,\n summary: \"List an automation's runs (staff)\",\n description:\n \"Staff only (owner, admin, instructor). A deleted automation's runs remain reachable as an audit trail, so an unknown id answers an empty page rather than 404. A run's event carries the fields a webhook delivery of that event would.\",\n params: AutomationIdParam,\n querystring: AutomationRunsQuery,\n response: { 200: AutomationRunsPage, ...refusals(403) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n // No existence pre-check: runs deliberately survive automation deletion (audit trail).\n return automations.listRuns(scope.orgId, req.params.id, req.query);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/automations/:id/runs/:runId/rerun',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'rerunAutomationRun',\n tags,\n summary: 'Run a finished run again (staff)',\n description:\n \"Staff only (owner, admin, instructor). Dispatches the same run again with the automation's current steps; the new outcome replaces the run's previous one. For a webhook this resends the delivery to its current URL. A run that is still running can't be rerun (409).\",\n params: AutomationRunParams,\n response: { 200: AutomationRun, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const scope = await resolveScope(container, req);\n return automations.rerun(scope.orgId, req.params.id, req.params.runId);\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"automations.js","names":[],"sources":["../../../src/http/routes/automations.ts"],"sourcesContent":["// HTTP routes for the automations context: rules that match a trigger (a\n// domain event type) against enabled automations and run an ordered list of\n// actions, plus the run history each automation accumulates.\nimport type { FastifyInstance } from 'fastify';\nimport { z } from 'zod';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport {\n Automation,\n AutomationIdParam,\n AutomationRun,\n AutomationRunParams,\n AutomationRunsPage,\n AutomationRunsQuery,\n AutomationsQuery,\n AvailableAction,\n AvailableTriggers,\n CreateAutomationBody,\n UpdateAutomationBody,\n refusals,\n} from '../schemas/index.js';\nimport type { Container } from '../../app/container.js';\nimport { resolveActor } from '../scope.js';\n\nexport async function automationsRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const automations = container.automations;\n const tags = ['Automations'];\n\n r.route({\n method: 'GET',\n url: '/api/automations',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listAutomations',\n tags,\n summary: 'List automations',\n description: `Requires \\`automations:read\\`. Optionally only workflows or only webhooks.`,\n querystring: AutomationsQuery,\n response: { 200: z.array(Automation), ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return automations.list(req.query, actor);\n },\n });\n\n // Before `/api/automations/:id` — otherwise `:id` would swallow these path segments.\n r.route({\n method: 'GET',\n url: '/api/automations/actions',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listAutomationActions',\n tags,\n summary: 'List the actions automations can use',\n description: 'Requires `automations:read`.',\n response: { 200: z.array(AvailableAction), ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return automations.availableActions(actor);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/automations/triggers',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listAutomationTriggers',\n tags,\n summary: 'List the domain events automations can react to',\n description: 'Requires `automations:read`.',\n response: { 200: AvailableTriggers, ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return automations.availableTriggers(actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/automations',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createAutomation',\n tags,\n summary: 'Create a workflow automation',\n description: `Requires \\`automations:write\\`. 400 when a trigger is reserved, not an event the system emits or an unsupported schedule, or an action is reserved for webhooks or no longer available.`,\n body: CreateAutomationBody,\n response: { 201: Automation, ...refusals(400, 403) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const automation = await automations.create(req.body, actor);\n return reply.code(201).send(automation);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/automations/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getAutomation',\n tags,\n summary: 'Get an automation',\n description: `Requires \\`automations:read\\`. Workflows and webhooks alike.`,\n params: AutomationIdParam,\n response: { 200: Automation, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return automations.get(req.params.id, actor);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/automations/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updateAutomation',\n tags,\n summary: 'Update a workflow automation',\n description: `Requires \\`automations:write\\`. Webhooks change through the webhook endpoints (409). 400 when a trigger is reserved, not an event the system emits or an unsupported schedule, or an action is reserved for webhooks or no longer available.`,\n params: AutomationIdParam,\n body: UpdateAutomationBody,\n response: { 200: Automation, ...refusals(400, 403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return automations.update(req.params.id, req.body, actor);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/automations/:id',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteAutomation',\n tags,\n summary: 'Delete a workflow automation',\n description: `Requires \\`automations:delete\\`. Webhooks are deleted through the webhook endpoints (409). Its runs stay readable.`,\n params: AutomationIdParam,\n response: { 204: z.void(), ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await automations.delete(req.params.id, actor);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/automations/:id/runs',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listAutomationRuns',\n tags,\n summary: \"List an automation's runs\",\n description: `Requires \\`automations:read\\`. A deleted automation's runs remain reachable as an audit trail, so an unknown id answers an empty page rather than 404. A run's event carries the fields a webhook delivery of that event would.`,\n params: AutomationIdParam,\n querystring: AutomationRunsQuery,\n response: { 200: AutomationRunsPage, ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n // No existence pre-check: runs deliberately survive automation deletion (audit trail).\n return automations.listRuns(req.params.id, req.query, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/automations/:id/runs/:runId/rerun',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'rerunAutomationRun',\n tags,\n summary: 'Run a finished run again',\n description: `Requires \\`automations:write\\`. Dispatches the same run again with the automation's current steps; the new outcome replaces the run's previous one. For a webhook this resends the delivery to its current URL. A run that is still running can't be rerun (409).`,\n params: AutomationRunParams,\n response: { 200: AutomationRun, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return automations.rerun(req.params.id, req.params.runId, actor);\n },\n });\n}\n"],"mappings":"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"}