@selvajs/server 1.1.0 → 1.3.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 (233) hide show
  1. package/dist/access/guards.d.ts +213 -0
  2. package/dist/access/guards.d.ts.map +1 -0
  3. package/dist/access/guards.js +417 -0
  4. package/dist/access/guards.js.map +1 -0
  5. package/dist/access/index.d.ts +2 -0
  6. package/dist/access/index.d.ts.map +1 -1
  7. package/dist/access/index.js +6 -0
  8. package/dist/access/index.js.map +1 -1
  9. package/dist/access/permissions-scope.d.ts +15 -0
  10. package/dist/access/permissions-scope.d.ts.map +1 -0
  11. package/dist/access/permissions-scope.js +23 -0
  12. package/dist/access/permissions-scope.js.map +1 -0
  13. package/dist/api/bodies.d.ts +104 -0
  14. package/dist/api/bodies.d.ts.map +1 -0
  15. package/dist/api/bodies.js +87 -0
  16. package/dist/api/bodies.js.map +1 -0
  17. package/dist/api/deps.d.ts +135 -0
  18. package/dist/api/deps.d.ts.map +1 -0
  19. package/dist/api/deps.js +55 -0
  20. package/dist/api/deps.js.map +1 -0
  21. package/dist/api/errors.d.ts +35 -0
  22. package/dist/api/errors.d.ts.map +1 -0
  23. package/dist/api/errors.js +63 -0
  24. package/dist/api/errors.js.map +1 -0
  25. package/dist/api/index.d.ts +12 -0
  26. package/dist/api/index.d.ts.map +1 -0
  27. package/dist/api/index.js +15 -0
  28. package/dist/api/index.js.map +1 -0
  29. package/dist/api/map-core-error.d.ts +19 -0
  30. package/dist/api/map-core-error.d.ts.map +1 -0
  31. package/dist/api/map-core-error.js +49 -0
  32. package/dist/api/map-core-error.js.map +1 -0
  33. package/dist/api/pagination.d.ts +19 -0
  34. package/dist/api/pagination.d.ts.map +1 -0
  35. package/dist/api/pagination.js +40 -0
  36. package/dist/api/pagination.js.map +1 -0
  37. package/dist/api/request.d.ts +93 -0
  38. package/dist/api/request.d.ts.map +1 -0
  39. package/dist/api/request.js +150 -0
  40. package/dist/api/request.js.map +1 -0
  41. package/dist/api/respond.d.ts +29 -0
  42. package/dist/api/respond.d.ts.map +1 -0
  43. package/dist/api/respond.js +59 -0
  44. package/dist/api/respond.js.map +1 -0
  45. package/dist/api/responses-schema.d.ts +158 -0
  46. package/dist/api/responses-schema.d.ts.map +1 -0
  47. package/dist/api/responses-schema.js +104 -0
  48. package/dist/api/responses-schema.js.map +1 -0
  49. package/dist/api/responses.d.ts +19 -0
  50. package/dist/api/responses.d.ts.map +1 -0
  51. package/dist/api/responses.js +21 -0
  52. package/dist/api/responses.js.map +1 -0
  53. package/dist/api/shaped.d.ts +23 -0
  54. package/dist/api/shaped.d.ts.map +1 -0
  55. package/dist/api/shaped.js +29 -0
  56. package/dist/api/shaped.js.map +1 -0
  57. package/dist/api/types.d.ts +41 -0
  58. package/dist/api/types.d.ts.map +1 -0
  59. package/dist/api/types.js +12 -0
  60. package/dist/api/types.js.map +1 -0
  61. package/dist/compute/errors.d.ts +18 -0
  62. package/dist/compute/errors.d.ts.map +1 -0
  63. package/dist/compute/errors.js +21 -0
  64. package/dist/compute/errors.js.map +1 -0
  65. package/dist/compute/evict-changed-servers.d.ts +22 -0
  66. package/dist/compute/evict-changed-servers.d.ts.map +1 -0
  67. package/dist/compute/evict-changed-servers.js +31 -0
  68. package/dist/compute/evict-changed-servers.js.map +1 -0
  69. package/dist/compute/idempotency-http.d.ts +47 -0
  70. package/dist/compute/idempotency-http.d.ts.map +1 -0
  71. package/dist/compute/idempotency-http.js +51 -0
  72. package/dist/compute/idempotency-http.js.map +1 -0
  73. package/dist/compute/index.d.ts +5 -0
  74. package/dist/compute/index.d.ts.map +1 -1
  75. package/dist/compute/index.js +5 -0
  76. package/dist/compute/index.js.map +1 -1
  77. package/dist/compute/resolve-server.d.ts +25 -0
  78. package/dist/compute/resolve-server.d.ts.map +1 -0
  79. package/dist/compute/resolve-server.js +35 -0
  80. package/dist/compute/resolve-server.js.map +1 -0
  81. package/dist/compute/server-config-write.d.ts +52 -0
  82. package/dist/compute/server-config-write.d.ts.map +1 -0
  83. package/dist/compute/server-config-write.js +100 -0
  84. package/dist/compute/server-config-write.js.map +1 -0
  85. package/dist/definitions/index.d.ts +1 -0
  86. package/dist/definitions/index.d.ts.map +1 -1
  87. package/dist/definitions/index.js +1 -0
  88. package/dist/definitions/index.js.map +1 -1
  89. package/dist/definitions/visibility.d.ts +74 -0
  90. package/dist/definitions/visibility.d.ts.map +1 -0
  91. package/dist/definitions/visibility.js +117 -0
  92. package/dist/definitions/visibility.js.map +1 -0
  93. package/dist/handlers/definitionVersions.d.ts +30 -0
  94. package/dist/handlers/definitionVersions.d.ts.map +1 -0
  95. package/dist/handlers/definitionVersions.js +93 -0
  96. package/dist/handlers/definitionVersions.js.map +1 -0
  97. package/dist/handlers/definitions.d.ts +35 -0
  98. package/dist/handlers/definitions.d.ts.map +1 -0
  99. package/dist/handlers/definitions.js +207 -0
  100. package/dist/handlers/definitions.js.map +1 -0
  101. package/dist/handlers/index.d.ts +27 -0
  102. package/dist/handlers/index.d.ts.map +1 -0
  103. package/dist/handlers/index.js +29 -0
  104. package/dist/handlers/index.js.map +1 -0
  105. package/dist/handlers/invite-lookup.d.ts +11 -0
  106. package/dist/handlers/invite-lookup.d.ts.map +1 -0
  107. package/dist/handlers/invite-lookup.js +20 -0
  108. package/dist/handlers/invite-lookup.js.map +1 -0
  109. package/dist/handlers/invites.d.ts +31 -0
  110. package/dist/handlers/invites.d.ts.map +1 -0
  111. package/dist/handlers/invites.js +148 -0
  112. package/dist/handlers/invites.js.map +1 -0
  113. package/dist/handlers/me.d.ts +8 -0
  114. package/dist/handlers/me.d.ts.map +1 -0
  115. package/dist/handlers/me.js +21 -0
  116. package/dist/handlers/me.js.map +1 -0
  117. package/dist/handlers/me.starred.d.ts +11 -0
  118. package/dist/handlers/me.starred.d.ts.map +1 -0
  119. package/dist/handlers/me.starred.js +21 -0
  120. package/dist/handlers/me.starred.js.map +1 -0
  121. package/dist/handlers/orgAssets.d.ts +13 -0
  122. package/dist/handlers/orgAssets.d.ts.map +1 -0
  123. package/dist/handlers/orgAssets.js +54 -0
  124. package/dist/handlers/orgAssets.js.map +1 -0
  125. package/dist/handlers/orgCompute.d.ts +16 -0
  126. package/dist/handlers/orgCompute.d.ts.map +1 -0
  127. package/dist/handlers/orgCompute.js +96 -0
  128. package/dist/handlers/orgCompute.js.map +1 -0
  129. package/dist/handlers/orgMembers.d.ts +29 -0
  130. package/dist/handlers/orgMembers.d.ts.map +1 -0
  131. package/dist/handlers/orgMembers.js +200 -0
  132. package/dist/handlers/orgMembers.js.map +1 -0
  133. package/dist/handlers/orgs.d.ts +16 -0
  134. package/dist/handlers/orgs.d.ts.map +1 -0
  135. package/dist/handlers/orgs.js +26 -0
  136. package/dist/handlers/orgs.js.map +1 -0
  137. package/dist/handlers/projectMembers.d.ts +15 -0
  138. package/dist/handlers/projectMembers.d.ts.map +1 -0
  139. package/dist/handlers/projectMembers.js +95 -0
  140. package/dist/handlers/projectMembers.js.map +1 -0
  141. package/dist/handlers/projects.d.ts +21 -0
  142. package/dist/handlers/projects.d.ts.map +1 -0
  143. package/dist/handlers/projects.js +164 -0
  144. package/dist/handlers/projects.js.map +1 -0
  145. package/dist/handlers/reclaim.d.ts +10 -0
  146. package/dist/handlers/reclaim.d.ts.map +1 -0
  147. package/dist/handlers/reclaim.js +38 -0
  148. package/dist/handlers/reclaim.js.map +1 -0
  149. package/dist/handlers/services.d.ts +22 -0
  150. package/dist/handlers/services.d.ts.map +1 -0
  151. package/dist/handlers/services.js +37 -0
  152. package/dist/handlers/services.js.map +1 -0
  153. package/dist/handlers/shareLinks.d.ts +23 -0
  154. package/dist/handlers/shareLinks.d.ts.map +1 -0
  155. package/dist/handlers/shareLinks.js +85 -0
  156. package/dist/handlers/shareLinks.js.map +1 -0
  157. package/dist/http/index.d.ts +1 -0
  158. package/dist/http/index.d.ts.map +1 -1
  159. package/dist/http/index.js +1 -0
  160. package/dist/http/index.js.map +1 -1
  161. package/dist/http/login-rate-limit.d.ts +72 -0
  162. package/dist/http/login-rate-limit.d.ts.map +1 -0
  163. package/dist/http/login-rate-limit.js +84 -0
  164. package/dist/http/login-rate-limit.js.map +1 -0
  165. package/dist/notifications/deliver-invite.d.ts +27 -0
  166. package/dist/notifications/deliver-invite.d.ts.map +1 -0
  167. package/dist/notifications/deliver-invite.js +32 -0
  168. package/dist/notifications/deliver-invite.js.map +1 -0
  169. package/dist/notifications/index.d.ts +4 -0
  170. package/dist/notifications/index.d.ts.map +1 -0
  171. package/dist/notifications/index.js +4 -0
  172. package/dist/notifications/index.js.map +1 -0
  173. package/dist/notifications/smtp-config.d.ts +37 -0
  174. package/dist/notifications/smtp-config.d.ts.map +1 -0
  175. package/dist/notifications/smtp-config.js +41 -0
  176. package/dist/notifications/smtp-config.js.map +1 -0
  177. package/dist/notifications/smtp-provider.d.ts +18 -0
  178. package/dist/notifications/smtp-provider.d.ts.map +1 -0
  179. package/dist/notifications/smtp-provider.js +71 -0
  180. package/dist/notifications/smtp-provider.js.map +1 -0
  181. package/dist/ops/deploymentConfig.d.ts +73 -0
  182. package/dist/ops/deploymentConfig.d.ts.map +1 -0
  183. package/dist/ops/deploymentConfig.js +154 -0
  184. package/dist/ops/deploymentConfig.js.map +1 -0
  185. package/dist/ops/index.d.ts +1 -0
  186. package/dist/ops/index.d.ts.map +1 -1
  187. package/dist/ops/index.js +3 -2
  188. package/dist/ops/index.js.map +1 -1
  189. package/dist/organizations/index.d.ts +3 -0
  190. package/dist/organizations/index.d.ts.map +1 -0
  191. package/dist/organizations/index.js +5 -0
  192. package/dist/organizations/index.js.map +1 -0
  193. package/dist/organizations/list-all-members.d.ts +10 -0
  194. package/dist/organizations/list-all-members.d.ts.map +1 -0
  195. package/dist/organizations/list-all-members.js +36 -0
  196. package/dist/organizations/list-all-members.js.map +1 -0
  197. package/dist/organizations/org-asset-service.d.ts +39 -0
  198. package/dist/organizations/org-asset-service.d.ts.map +1 -0
  199. package/dist/organizations/org-asset-service.js +71 -0
  200. package/dist/organizations/org-asset-service.js.map +1 -0
  201. package/dist/projects/create-project.d.ts +30 -0
  202. package/dist/projects/create-project.d.ts.map +1 -0
  203. package/dist/projects/create-project.js +56 -0
  204. package/dist/projects/create-project.js.map +1 -0
  205. package/dist/projects/index.d.ts +2 -0
  206. package/dist/projects/index.d.ts.map +1 -0
  207. package/dist/projects/index.js +3 -0
  208. package/dist/projects/index.js.map +1 -0
  209. package/dist/providers/auth-lookup.d.ts +14 -0
  210. package/dist/providers/auth-lookup.d.ts.map +1 -0
  211. package/dist/providers/auth-lookup.js +34 -0
  212. package/dist/providers/auth-lookup.js.map +1 -0
  213. package/dist/providers/index.d.ts +1 -0
  214. package/dist/providers/index.d.ts.map +1 -1
  215. package/dist/providers/index.js +1 -0
  216. package/dist/providers/index.js.map +1 -1
  217. package/dist/testing/harness.d.ts +168 -0
  218. package/dist/testing/harness.d.ts.map +1 -0
  219. package/dist/testing/harness.js +314 -0
  220. package/dist/testing/harness.js.map +1 -0
  221. package/dist/testing/index.d.ts +18 -0
  222. package/dist/testing/index.d.ts.map +1 -0
  223. package/dist/testing/index.js +15 -0
  224. package/dist/testing/index.js.map +1 -0
  225. package/dist/testing/scenarios.d.ts +67 -0
  226. package/dist/testing/scenarios.d.ts.map +1 -0
  227. package/dist/testing/scenarios.js +104 -0
  228. package/dist/testing/scenarios.js.map +1 -0
  229. package/dist/testing/seed-adapter.d.ts +31 -0
  230. package/dist/testing/seed-adapter.d.ts.map +1 -0
  231. package/dist/testing/seed-adapter.js +19 -0
  232. package/dist/testing/seed-adapter.js.map +1 -0
  233. package/package.json +42 -3
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Org owner/admin escape hatch for a project they cannot otherwise reach.
3
+ *
4
+ * Adds the actor as a co-owner and does NOT demote the existing owner.
5
+ * Idempotent: reclaiming twice is a no-op, because `addProjectMember`
6
+ * reactivates a soft-deleted row in place.
7
+ */
8
+ import { created, requireParams } from '../api/index.js';
9
+ import { actorFrom } from '@selvajs/platform';
10
+ import { requireCanReclaim } from '../access/index.js';
11
+ export const reclaimProject = async (req) => {
12
+ const { id } = requireParams(req.params, 'id');
13
+ const { user, ctx, project } = await requireCanReclaim(req, id);
14
+ const now = new Date().toISOString();
15
+ const member = {
16
+ projectId: id,
17
+ userId: user.id,
18
+ role: 'owner',
19
+ joinedAt: now,
20
+ updatedAt: now,
21
+ updatedBy: ctx.userId || user.id,
22
+ deletedAt: null
23
+ };
24
+ await req.deps.projects.addProjectMember(ctx, member);
25
+ // `addProjectMember` emits `project_member.added`, which is byte-identical
26
+ // to an owner adding a teammate. The whole escape hatch rests on the audit
27
+ // trail, so the escalation needs its own event or the log cannot answer the
28
+ // only question an auditor has here.
29
+ await req.deps.events.emit({
30
+ type: 'project.reclaimed',
31
+ projectId: id,
32
+ orgId: project.orgId,
33
+ actorId: actorFrom(ctx),
34
+ priorVisibility: project.visibility
35
+ });
36
+ return created({ member });
37
+ };
38
+ //# sourceMappingURL=reclaim.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reclaim.js","sourceRoot":"","sources":["../../src/handlers/reclaim.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,aAAa,EAAmB,MAAM,iBAAiB,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAsB,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,CAAC,MAAM,cAAc,GAAe,KAAK,EAAE,GAAG,EAAE,EAAE;IACvD,MAAM,EAAE,EAAE,EAAE,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC/C,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,MAAM,iBAAiB,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAEhE,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACrC,MAAM,MAAM,GAAkB;QAC7B,SAAS,EAAE,EAAE;QACb,MAAM,EAAE,IAAI,CAAC,EAAE;QACf,IAAI,EAAE,OAAO;QACb,QAAQ,EAAE,GAAG;QACb,SAAS,EAAE,GAAG;QACd,SAAS,EAAE,GAAG,CAAC,MAAM,IAAI,IAAI,CAAC,EAAE;QAChC,SAAS,EAAE,IAAI;KACf,CAAC;IAEF,MAAM,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAEtD,2EAA2E;IAC3E,2EAA2E;IAC3E,4EAA4E;IAC5E,qCAAqC;IACrC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;QAC1B,IAAI,EAAE,mBAAmB;QACzB,SAAS,EAAE,EAAE;QACb,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,OAAO,EAAE,SAAS,CAAC,GAAG,CAAC;QACvB,eAAe,EAAE,OAAO,CAAC,UAAU;KACnC,CAAC,CAAC;IAEH,OAAO,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;AAC5B,CAAC,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Accessors for the host-composed services on `SelvaDeps`.
3
+ *
4
+ * `services` is host-supplied and optional by type, so every call site would
5
+ * otherwise repeat a presence check or reach for `!`. A host that mounts these
6
+ * handlers without wiring a service it needs gets a named error here rather
7
+ * than `Cannot read properties of undefined` from inside the service call.
8
+ */
9
+ import type { SelvaDeps } from '../api/index.js';
10
+ import type { TokenCodec } from '../tokens/index.js';
11
+ import type { OrgAssetService } from '../organizations/index.js';
12
+ export declare function definitionService(deps: SelvaDeps): import("../definitions/definition-service.js").DefinitionService;
13
+ export declare function orgAssetService(deps: SelvaDeps): OrgAssetService;
14
+ /**
15
+ * A token codec, or a named failure.
16
+ *
17
+ * Minting through an absent codec is the one failure mode worth being loud
18
+ * about: a handler that quietly skipped hashing would persist a row whose token
19
+ * can never be verified, and nothing downstream would report it.
20
+ */
21
+ export declare function tokenCodec(deps: SelvaDeps, family: keyof SelvaDeps['tokens']): TokenCodec;
22
+ //# sourceMappingURL=services.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../../src/handlers/services.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAEjE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,oEAMhD;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,GAAG,eAAe,CAMhE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC,QAAQ,CAAC,GAAG,UAAU,CAMzF"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Accessors for the host-composed services on `SelvaDeps`.
3
+ *
4
+ * `services` is host-supplied and optional by type, so every call site would
5
+ * otherwise repeat a presence check or reach for `!`. A host that mounts these
6
+ * handlers without wiring a service it needs gets a named error here rather
7
+ * than `Cannot read properties of undefined` from inside the service call.
8
+ */
9
+ export function definitionService(deps) {
10
+ const service = deps.services.definitions;
11
+ if (!service) {
12
+ throw new Error('SelvaDeps.services.definitions is not wired — mount requires it.');
13
+ }
14
+ return service;
15
+ }
16
+ export function orgAssetService(deps) {
17
+ const service = deps.services.orgAssets;
18
+ if (!service) {
19
+ throw new Error('SelvaDeps.services.orgAssets is not wired — mount requires it.');
20
+ }
21
+ return service;
22
+ }
23
+ /**
24
+ * A token codec, or a named failure.
25
+ *
26
+ * Minting through an absent codec is the one failure mode worth being loud
27
+ * about: a handler that quietly skipped hashing would persist a row whose token
28
+ * can never be verified, and nothing downstream would report it.
29
+ */
30
+ export function tokenCodec(deps, family) {
31
+ const codec = deps.tokens[family];
32
+ if (!codec) {
33
+ throw new Error(`SelvaDeps.tokens.${family} is not wired — mount requires it.`);
34
+ }
35
+ return codec;
36
+ }
37
+ //# sourceMappingURL=services.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"services.js","sourceRoot":"","sources":["../../src/handlers/services.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,MAAM,UAAU,iBAAiB,CAAC,IAAe;IAChD,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC;IAC1C,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;IACrF,CAAC;IACD,OAAO,OAAO,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAe;IAC9C,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;IACxC,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAC;IACnF,CAAC;IACD,OAAO,OAAO,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,IAAe,EAAE,MAAiC;IAC5E,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,oBAAoB,MAAM,oCAAoC,CAAC,CAAC;IACjF,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Share-link administration.
3
+ *
4
+ * Minting, listing and revoking are gated by `canEditDefinition` — the same
5
+ * authority that uploads versions and publishes. The raw token appears in the
6
+ * POST response and nowhere else; everything afterwards sees `hasToken: true`.
7
+ *
8
+ * **Responses go through `shaped`/`shapedCollection`, never `{ body }`.** A
9
+ * `ShareLink` carries `tokenHash`, and the response schema is what drops it —
10
+ * returning the record directly would put a credential hash on the wire with
11
+ * nothing failing at build time.
12
+ */
13
+ import type { ApiHandler } from '../api/index.js';
14
+ export declare const listShareLinks: ApiHandler;
15
+ export declare const createShareLink: ApiHandler;
16
+ /**
17
+ * Revoke a share link (soft-delete). Idempotent, gated by `canEditDefinition`.
18
+ *
19
+ * The link must belong to this definition, so one definition's editor cannot
20
+ * revoke another's link with a guessed or leaked linkId.
21
+ */
22
+ export declare const revokeShareLink: ApiHandler;
23
+ //# sourceMappingURL=shareLinks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shareLinks.d.ts","sourceRoot":"","sources":["../../src/handlers/shareLinks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAgBH,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,iBAAiB,CAAC;AAsB9D,eAAO,MAAM,cAAc,EAAE,UAU5B,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,UA8B7B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,UAc7B,CAAC"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Share-link administration.
3
+ *
4
+ * Minting, listing and revoking are gated by `canEditDefinition` — the same
5
+ * authority that uploads versions and publishes. The raw token appears in the
6
+ * POST response and nowhere else; everything afterwards sees `hasToken: true`.
7
+ *
8
+ * **Responses go through `shaped`/`shapedCollection`, never `{ body }`.** A
9
+ * `ShareLink` carries `tokenHash`, and the response schema is what drops it —
10
+ * returning the record directly would put a credential hash on the wire with
11
+ * nothing failing at build time.
12
+ */
13
+ import { randomUUID } from 'node:crypto';
14
+ import { apiError, ApiErrorCode, CreatedShareLinkResponseSchema, noContent, parseBody, parseListOptions, parseParam, requireCaller, shaped, shapedCollection, ShareLinkResponseSchema } from '../api/index.js';
15
+ import { CreateShareLinkInputSchema, DEFAULT_SHARE_LINK_MAX_SOLVES } from '@selvajs/platform';
16
+ import { GuidSchema } from '@selvajs/platform/definitions';
17
+ import { requireEditableDefinition } from '../access/index.js';
18
+ import { tokenCodec } from './services.js';
19
+ function assertSharingEnabled(req) {
20
+ if (!req.deps.flag('ENABLE_SHARING')) {
21
+ apiError(404, ApiErrorCode.NOT_FOUND, 'Share links are disabled on this instance (ENABLE_SHARING).');
22
+ }
23
+ }
24
+ /** `tokenHash` is dropped by the response schema, not by this function. */
25
+ function forClient(link) {
26
+ return { ...link, hasToken: true };
27
+ }
28
+ export const listShareLinks = async (req) => {
29
+ assertSharingEnabled(req);
30
+ const guid = parseParam(req.params.guid, GuidSchema, 'GUID');
31
+ const { ctx } = await requireEditableDefinition(req, guid);
32
+ const page = await req.deps.shareLinks.listByDefinition(ctx, guid, parseListOptions(req.url));
33
+ return shapedCollection(ShareLinkResponseSchema, {
34
+ items: page.items.map(forClient),
35
+ nextCursor: page.nextCursor
36
+ });
37
+ };
38
+ export const createShareLink = async (req) => {
39
+ assertSharingEnabled(req);
40
+ const guid = parseParam(req.params.guid, GuidSchema, 'GUID');
41
+ const { user } = requireCaller(req);
42
+ const { ctx } = await requireEditableDefinition(req, guid);
43
+ const input = await parseBody(req.request, CreateShareLinkInputSchema, { missingAs: {} });
44
+ const codec = tokenCodec(req.deps, 'shareLinks');
45
+ const raw = codec.mintRawToken();
46
+ const link = {
47
+ id: randomUUID(),
48
+ definitionId: guid,
49
+ channel: input.channel,
50
+ tokenHash: codec.hashToken(raw),
51
+ name: input.name,
52
+ createdBy: user.id,
53
+ createdAt: new Date().toISOString(),
54
+ expiresAt: input.expiresAt ?? null,
55
+ revokedAt: null,
56
+ allowSolve: input.allowSolve,
57
+ // The default applies only when the field is absent. An explicit `null`
58
+ // is a deliberate "uncap" choice and is preserved.
59
+ maxSolves: input.maxSolves === undefined ? DEFAULT_SHARE_LINK_MAX_SOLVES : input.maxSolves,
60
+ solveCount: 0
61
+ };
62
+ await req.deps.shareLinks.create(ctx, link);
63
+ // The token is returned once — a client that loses it must revoke and re-mint.
64
+ return shaped(CreatedShareLinkResponseSchema, { link: forClient(link), token: raw }, 201);
65
+ };
66
+ /**
67
+ * Revoke a share link (soft-delete). Idempotent, gated by `canEditDefinition`.
68
+ *
69
+ * The link must belong to this definition, so one definition's editor cannot
70
+ * revoke another's link with a guessed or leaked linkId.
71
+ */
72
+ export const revokeShareLink = async (req) => {
73
+ assertSharingEnabled(req);
74
+ const guid = parseParam(req.params.guid, GuidSchema, 'GUID');
75
+ const linkId = parseParam(req.params.linkId, GuidSchema, 'link ID');
76
+ const { ctx } = await requireEditableDefinition(req, guid);
77
+ const existing = await req.deps.shareLinks.getById(ctx, linkId);
78
+ if (!existing || existing.definitionId !== guid) {
79
+ // 404 either way — never disclose that the id exists on another definition.
80
+ apiError(404, ApiErrorCode.NOT_FOUND, 'Share link not found.');
81
+ }
82
+ await req.deps.shareLinks.revoke(ctx, linkId);
83
+ return noContent();
84
+ };
85
+ //# sourceMappingURL=shareLinks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shareLinks.js","sourceRoot":"","sources":["../../src/handlers/shareLinks.ts"],"names":[],"mappings":"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"}
@@ -2,4 +2,5 @@ export { safeRedirectTarget } from './redirect.js';
2
2
  export { declaredBodySizeExceeds, type HeadersLike } from './body-size.js';
3
3
  export { applySecurityHeaders, type SecurityHeaderOptions } from './security-headers.js';
4
4
  export { createRouteClassifier, type RouteClassifier, type RouteClassifierConfig } from './route-classifier.js';
5
+ export { createLoginRateLimiter, addressKeysCollapsed, type LoginRateLimiter, type LoginRateLimiterConfig } from './login-rate-limit.js';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACzF,OAAO,EACN,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAE,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACzF,OAAO,EACN,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACN,sBAAsB,EACtB,oBAAoB,EACpB,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,MAAM,uBAAuB,CAAC"}
@@ -3,4 +3,5 @@ export { safeRedirectTarget } from './redirect.js';
3
3
  export { declaredBodySizeExceeds } from './body-size.js';
4
4
  export { applySecurityHeaders } from './security-headers.js';
5
5
  export { createRouteClassifier } from './route-classifier.js';
6
+ export { createLoginRateLimiter, addressKeysCollapsed } from './login-rate-limit.js';
6
7
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAoB,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAA8B,MAAM,uBAAuB,CAAC;AACzF,OAAO,EACN,qBAAqB,EAGrB,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAoB,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAA8B,MAAM,uBAAuB,CAAC;AACzF,OAAO,EACN,qBAAqB,EAGrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACN,sBAAsB,EACtB,oBAAoB,EAGpB,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Login rate limiting: two buckets per attempt, because neither dimension
3
+ * covers the other.
4
+ *
5
+ * - **Per-address** bounds one client hammering the form. It is only
6
+ * meaningful when the host resolves a real client address. Behind a reverse
7
+ * proxy that is not configured to forward one, every request reports the
8
+ * socket peer — `127.0.0.1` for all of them — which collapses the whole key
9
+ * space into a single bucket: five failed logins from anywhere lock out the
10
+ * entire instance, and only a success clears it, which nobody can now
11
+ * reach. That failure is completely silent, hence
12
+ * {@link addressKeysCollapsed}.
13
+ * - **Per-account** bounds a targeted guessing attack, which address limiting
14
+ * does not: an attacker spread across many source IPs contends with no
15
+ * shared counter at all, leaving password-hash cost as the only real bound
16
+ * on online guessing.
17
+ *
18
+ * An attempt must clear both and a failure charges both. A success clears the
19
+ * account bucket and the calling address's — a *different* address that failed
20
+ * against the same account keeps its own penalty.
21
+ *
22
+ * The flow counts failures rather than attempts: `check` gates without spending
23
+ * budget, `recordFailure` spends, and `clear` forgives. A successful login
24
+ * therefore costs nothing, so an ordinary user typing one wrong password never
25
+ * walks toward a lockout.
26
+ */
27
+ import type { RateLimitResult } from '../compute/rate-limit.js';
28
+ export interface LoginRateLimiterConfig {
29
+ /** Window for both buckets. Default 15 minutes. */
30
+ windowMs?: number;
31
+ /** Failed logins per address per window. Default 5. */
32
+ maxPerAddress?: number;
33
+ /**
34
+ * Failed logins per account per window. Default 20 — deliberately looser
35
+ * than the address cap, because this bucket is reachable by an attacker who
36
+ * wants to lock a known user out of their own account. It trades a little
37
+ * brute-force headroom for not being a cheap denial of service on a named
38
+ * victim.
39
+ */
40
+ maxPerAccount?: number;
41
+ }
42
+ export interface LoginRateLimiter {
43
+ /**
44
+ * Gate one attempt without spending budget.
45
+ *
46
+ * `email` may be absent — a password-only login against a provider with no
47
+ * user store has no account to key on — in which case only the address
48
+ * bucket applies.
49
+ */
50
+ check(address: string, email?: string): RateLimitResult;
51
+ /** Charge a failed attempt to both buckets. */
52
+ recordFailure(address: string, email?: string): void;
53
+ /** Forgive on success. */
54
+ clear(address: string, email?: string): void;
55
+ /** Drop all state. For tests. */
56
+ reset(): void;
57
+ }
58
+ export declare function createLoginRateLimiter(config?: LoginRateLimiterConfig): LoginRateLimiter;
59
+ /**
60
+ * Whether per-address limiting has degenerated into one shared bucket.
61
+ *
62
+ * True when the host reports a loopback address for a request it did not
63
+ * originate — which means it sits behind a proxy and is reading the socket peer
64
+ * instead of a forwarded client address. The host decides what to do about it;
65
+ * warning once at startup is enough, and warning per request is not.
66
+ *
67
+ * `addressHeaderConfigured` is the host's answer to "am I reading a forwarded
68
+ * header at all", since which header and how many hops to trust are deployment
69
+ * config that no package can resolve for it.
70
+ */
71
+ export declare function addressKeysCollapsed(address: string, addressHeaderConfigured: boolean): boolean;
72
+ //# sourceMappingURL=login-rate-limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login-rate-limit.d.ts","sourceRoot":"","sources":["../../src/http/login-rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAKhE,MAAM,WAAW,sBAAsB;IACtC,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAChC;;;;;;OAMG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,eAAe,CAAC;IACxD,+CAA+C;IAC/C,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrD,0BAA0B;IAC1B,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,iCAAiC;IACjC,KAAK,IAAI,IAAI,CAAC;CACd;AAOD,wBAAgB,sBAAsB,CAAC,MAAM,GAAE,sBAA2B,GAAG,gBAAgB,CA8B5F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,uBAAuB,EAAE,OAAO,GAAG,OAAO,CAG/F"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Login rate limiting: two buckets per attempt, because neither dimension
3
+ * covers the other.
4
+ *
5
+ * - **Per-address** bounds one client hammering the form. It is only
6
+ * meaningful when the host resolves a real client address. Behind a reverse
7
+ * proxy that is not configured to forward one, every request reports the
8
+ * socket peer — `127.0.0.1` for all of them — which collapses the whole key
9
+ * space into a single bucket: five failed logins from anywhere lock out the
10
+ * entire instance, and only a success clears it, which nobody can now
11
+ * reach. That failure is completely silent, hence
12
+ * {@link addressKeysCollapsed}.
13
+ * - **Per-account** bounds a targeted guessing attack, which address limiting
14
+ * does not: an attacker spread across many source IPs contends with no
15
+ * shared counter at all, leaving password-hash cost as the only real bound
16
+ * on online guessing.
17
+ *
18
+ * An attempt must clear both and a failure charges both. A success clears the
19
+ * account bucket and the calling address's — a *different* address that failed
20
+ * against the same account keeps its own penalty.
21
+ *
22
+ * The flow counts failures rather than attempts: `check` gates without spending
23
+ * budget, `recordFailure` spends, and `clear` forgives. A successful login
24
+ * therefore costs nothing, so an ordinary user typing one wrong password never
25
+ * walks toward a lockout.
26
+ */
27
+ import { createComputeRateLimiter } from '../compute/rate-limit.js';
28
+ /** Loopback in the three spellings a Node server actually reports. */
29
+ const LOOPBACK = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
30
+ /** Normalized so case tricks (`Alice@` vs `alice@`) share one bucket. */
31
+ function accountKey(email) {
32
+ return `account:${email.trim().toLowerCase()}`;
33
+ }
34
+ export function createLoginRateLimiter(config = {}) {
35
+ const windowMs = config.windowMs ?? 15 * 60 * 1000;
36
+ const byAddress = createComputeRateLimiter({
37
+ windowMs,
38
+ maxPerWindow: config.maxPerAddress ?? 5
39
+ });
40
+ const byAccount = createComputeRateLimiter({
41
+ windowMs,
42
+ maxPerWindow: config.maxPerAccount ?? 20
43
+ });
44
+ return {
45
+ check(address, email) {
46
+ const address_ = byAddress.peek(address);
47
+ if (!address_.allowed || !email)
48
+ return address_;
49
+ return byAccount.peek(accountKey(email));
50
+ },
51
+ recordFailure(address, email) {
52
+ byAddress.check(address);
53
+ if (email)
54
+ byAccount.check(accountKey(email));
55
+ },
56
+ clear(address, email) {
57
+ byAddress.clear(address);
58
+ if (email)
59
+ byAccount.clear(accountKey(email));
60
+ },
61
+ reset() {
62
+ byAddress.reset();
63
+ byAccount.reset();
64
+ }
65
+ };
66
+ }
67
+ /**
68
+ * Whether per-address limiting has degenerated into one shared bucket.
69
+ *
70
+ * True when the host reports a loopback address for a request it did not
71
+ * originate — which means it sits behind a proxy and is reading the socket peer
72
+ * instead of a forwarded client address. The host decides what to do about it;
73
+ * warning once at startup is enough, and warning per request is not.
74
+ *
75
+ * `addressHeaderConfigured` is the host's answer to "am I reading a forwarded
76
+ * header at all", since which header and how many hops to trust are deployment
77
+ * config that no package can resolve for it.
78
+ */
79
+ export function addressKeysCollapsed(address, addressHeaderConfigured) {
80
+ if (addressHeaderConfigured)
81
+ return false;
82
+ return LOOPBACK.has(address);
83
+ }
84
+ //# sourceMappingURL=login-rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login-rate-limit.js","sourceRoot":"","sources":["../../src/http/login-rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AAGpE,sEAAsE;AACtE,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,KAAK,EAAE,kBAAkB,CAAC,CAAC,CAAC;AAkCnE,yEAAyE;AACzE,SAAS,UAAU,CAAC,KAAa;IAChC,OAAO,WAAW,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,SAAiC,EAAE;IACzE,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IACnD,MAAM,SAAS,GAAG,wBAAwB,CAAC;QAC1C,QAAQ;QACR,YAAY,EAAE,MAAM,CAAC,aAAa,IAAI,CAAC;KACvC,CAAC,CAAC;IACH,MAAM,SAAS,GAAG,wBAAwB,CAAC;QAC1C,QAAQ;QACR,YAAY,EAAE,MAAM,CAAC,aAAa,IAAI,EAAE;KACxC,CAAC,CAAC;IAEH,OAAO;QACN,KAAK,CAAC,OAAO,EAAE,KAAK;YACnB,MAAM,QAAQ,GAAG,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACzC,IAAI,CAAC,QAAQ,CAAC,OAAO,IAAI,CAAC,KAAK;gBAAE,OAAO,QAAQ,CAAC;YACjD,OAAO,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1C,CAAC;QACD,aAAa,CAAC,OAAO,EAAE,KAAK;YAC3B,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACzB,IAAI,KAAK;gBAAE,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;QAC/C,CAAC;QACD,KAAK,CAAC,OAAO,EAAE,KAAK;YACnB,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACzB,IAAI,KAAK;gBAAE,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;QAC/C,CAAC;QACD,KAAK;YACJ,SAAS,CAAC,KAAK,EAAE,CAAC;YAClB,SAAS,CAAC,KAAK,EAAE,CAAC;QACnB,CAAC;KACD,CAAC;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAe,EAAE,uBAAgC;IACrF,IAAI,uBAAuB;QAAE,OAAO,KAAK,CAAC;IAC1C,OAAO,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;AAC9B,CAAC"}
@@ -0,0 +1,27 @@
1
+ import type { AuthUser, IDataProvider, ILogger, INotificationProvider, Invite, RequestContext, UserProfile } from '@selvajs/platform';
2
+ export type InviteDelivery = 'sent' | 'not-configured' | 'failed';
3
+ export interface DeliverInviteInput {
4
+ ctx: RequestContext;
5
+ log: ILogger;
6
+ invite: Invite;
7
+ acceptUrl: string;
8
+ /** The admin minting the invite — used only for the "X invited you" line. */
9
+ actor: {
10
+ profile?: UserProfile;
11
+ user?: AuthUser;
12
+ };
13
+ orgs: IDataProvider['orgs'];
14
+ notifications: INotificationProvider;
15
+ /** Falls back into the org-name slot when the org has no name of its own. */
16
+ fallbackOrgName: string;
17
+ }
18
+ /**
19
+ * Mail an invite's accept link, best-effort.
20
+ *
21
+ * Shared by the mint and resend routes so they cannot disagree about what the
22
+ * invitee receives. Swallows every failure: the invite row is already
23
+ * committed and the caller still holds `acceptUrl` to share by hand, so a dead
24
+ * SMTP host must not report a valid invite as a failure.
25
+ */
26
+ export declare function deliverInvite(input: DeliverInviteInput): Promise<InviteDelivery>;
27
+ //# sourceMappingURL=deliver-invite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deliver-invite.d.ts","sourceRoot":"","sources":["../../src/notifications/deliver-invite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,QAAQ,EACR,aAAa,EACb,OAAO,EACP,qBAAqB,EACrB,MAAM,EACN,cAAc,EACd,WAAW,EACX,MAAM,mBAAmB,CAAC;AAG3B,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,gBAAgB,GAAG,QAAQ,CAAC;AAElE,MAAM,WAAW,kBAAkB;IAClC,GAAG,EAAE,cAAc,CAAC;IACpB,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,KAAK,EAAE;QAAE,OAAO,CAAC,EAAE,WAAW,CAAC;QAAC,IAAI,CAAC,EAAE,QAAQ,CAAA;KAAE,CAAC;IAClD,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC5B,aAAa,EAAE,qBAAqB,CAAC;IACrC,6EAA6E;IAC7E,eAAe,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC,CAuBtF"}
@@ -0,0 +1,32 @@
1
+ import { renderInviteEmail } from '@selvajs/notifications';
2
+ /**
3
+ * Mail an invite's accept link, best-effort.
4
+ *
5
+ * Shared by the mint and resend routes so they cannot disagree about what the
6
+ * invitee receives. Swallows every failure: the invite row is already
7
+ * committed and the caller still holds `acceptUrl` to share by hand, so a dead
8
+ * SMTP host must not report a valid invite as a failure.
9
+ */
10
+ export async function deliverInvite(input) {
11
+ const { ctx, log, invite, acceptUrl, actor, orgs, notifications, fallbackOrgName } = input;
12
+ try {
13
+ const org = await orgs.getOrg(ctx, invite.orgId);
14
+ const { status } = await notifications.send(renderInviteEmail({
15
+ to: invite.email,
16
+ acceptUrl,
17
+ orgName: org?.name ?? fallbackOrgName,
18
+ invitedBy: actor.profile?.displayName || actor.user?.email,
19
+ expiresAt: invite.expiresAt
20
+ }), log);
21
+ return status;
22
+ }
23
+ catch (err) {
24
+ log.warn('Invite mail could not be prepared', {
25
+ component: 'Invites',
26
+ inviteId: invite.id,
27
+ reason: err instanceof Error ? err.message : String(err)
28
+ });
29
+ return 'failed';
30
+ }
31
+ }
32
+ //# sourceMappingURL=deliver-invite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deliver-invite.js","sourceRoot":"","sources":["../../src/notifications/deliver-invite.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAiB3D;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAyB;IAC5D,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,eAAe,EAAE,GAAG,KAAK,CAAC;IAC3F,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;QACjD,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,IAAI,CAC1C,iBAAiB,CAAC;YACjB,EAAE,EAAE,MAAM,CAAC,KAAK;YAChB,SAAS;YACT,OAAO,EAAE,GAAG,EAAE,IAAI,IAAI,eAAe;YACrC,SAAS,EAAE,KAAK,CAAC,OAAO,EAAE,WAAW,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK;YAC1D,SAAS,EAAE,MAAM,CAAC,SAAS;SAC3B,CAAC,EACF,GAAG,CACH,CAAC;QACF,OAAO,MAAM,CAAC;IACf,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,GAAG,CAAC,IAAI,CAAC,mCAAmC,EAAE;YAC7C,SAAS,EAAE,SAAS;YACpB,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,MAAM,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;SACxD,CAAC,CAAC;QACH,OAAO,QAAQ,CAAC;IACjB,CAAC;AACF,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { SmtpNotificationProvider } from './smtp-provider.js';
2
+ export { readSmtpConfig, type SmtpConfig, type SmtpEnv } from './smtp-config.js';
3
+ export { deliverInvite, type DeliverInviteInput, type InviteDelivery } from './deliver-invite.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,KAAK,UAAU,EAAE,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { SmtpNotificationProvider } from './smtp-provider.js';
2
+ export { readSmtpConfig } from './smtp-config.js';
3
+ export { deliverInvite } from './deliver-invite.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAiC,MAAM,kBAAkB,CAAC;AACjF,OAAO,EAAE,aAAa,EAAgD,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,37 @@
1
+ export interface SmtpConfig {
2
+ host: string;
3
+ port: number;
4
+ secure: boolean;
5
+ auth?: {
6
+ user: string;
7
+ pass: string;
8
+ };
9
+ from: string;
10
+ }
11
+ /** The env vars this provider reads. The app passes `process.env` or its framework equivalent. */
12
+ export interface SmtpEnv {
13
+ SMTP_HOST?: string;
14
+ SMTP_PORT?: string;
15
+ SMTP_USER?: string;
16
+ SMTP_PASS?: string;
17
+ SMTP_FROM?: string;
18
+ SMTP_SECURE?: string;
19
+ [key: string]: string | undefined;
20
+ }
21
+ /**
22
+ * Read SMTP settings from an env bag. Taken as a parameter rather than read
23
+ * from `process.env` directly: under `vite dev` the framework does not mirror
24
+ * `.env` into `process.env`, so the app must hand over its own resolved env
25
+ * (`$env/dynamic/private` in SvelteKit) or overrides silently fall back to
26
+ * defaults.
27
+ *
28
+ * Returns null when mail is not configured — the caller treats that as "no
29
+ * mail", not as an error.
30
+ *
31
+ * Throws only when `SMTP_HOST` is set but `SMTP_FROM` is missing: that
32
+ * combination is a half-finished config, and failing loudly beats silently
33
+ * never sending. Every message needs an envelope sender, and guessing one
34
+ * produces mail that fails SPF at the recipient.
35
+ */
36
+ export declare function readSmtpConfig(env: SmtpEnv): SmtpConfig | null;
37
+ //# sourceMappingURL=smtp-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smtp-config.d.ts","sourceRoot":"","sources":["../../src/notifications/smtp-config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,IAAI,EAAE,MAAM,CAAC;CACb;AAED,kGAAkG;AAClG,MAAM,WAAW,OAAO;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IAKrB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CAClC;AAOD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,UAAU,GAAG,IAAI,CAsB9D"}
@@ -0,0 +1,41 @@
1
+ function parsePort(raw) {
2
+ const n = Number(raw ?? 587);
3
+ return Number.isInteger(n) && n > 0 && n < 65536 ? n : 587;
4
+ }
5
+ /**
6
+ * Read SMTP settings from an env bag. Taken as a parameter rather than read
7
+ * from `process.env` directly: under `vite dev` the framework does not mirror
8
+ * `.env` into `process.env`, so the app must hand over its own resolved env
9
+ * (`$env/dynamic/private` in SvelteKit) or overrides silently fall back to
10
+ * defaults.
11
+ *
12
+ * Returns null when mail is not configured — the caller treats that as "no
13
+ * mail", not as an error.
14
+ *
15
+ * Throws only when `SMTP_HOST` is set but `SMTP_FROM` is missing: that
16
+ * combination is a half-finished config, and failing loudly beats silently
17
+ * never sending. Every message needs an envelope sender, and guessing one
18
+ * produces mail that fails SPF at the recipient.
19
+ */
20
+ export function readSmtpConfig(env) {
21
+ const host = env.SMTP_HOST?.trim();
22
+ if (!host)
23
+ return null;
24
+ const from = env.SMTP_FROM?.trim();
25
+ if (!from) {
26
+ throw new Error('SMTP_HOST is set but SMTP_FROM is missing — mail needs a sender address.');
27
+ }
28
+ const port = parsePort(env.SMTP_PORT);
29
+ const user = env.SMTP_USER?.trim();
30
+ const pass = env.SMTP_PASS;
31
+ return {
32
+ host,
33
+ port,
34
+ // Port 465 is implicit TLS; 587 and 25 start plaintext and upgrade via
35
+ // STARTTLS, which nodemailer does on its own when `secure` is false.
36
+ secure: env.SMTP_SECURE ? env.SMTP_SECURE === 'true' : port === 465,
37
+ auth: user && pass ? { user, pass } : undefined,
38
+ from
39
+ };
40
+ }
41
+ //# sourceMappingURL=smtp-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smtp-config.js","sourceRoot":"","sources":["../../src/notifications/smtp-config.ts"],"names":[],"mappings":"AAuBA,SAAS,SAAS,CAAC,GAAuB;IACzC,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC;IAC7B,OAAO,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AAC5D,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,cAAc,CAAC,GAAY;IAC1C,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC;IACnC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IAEvB,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC;IACnC,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,0EAA0E,CAAC,CAAC;IAC7F,CAAC;IAED,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC;IACnC,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,CAAC;IAE3B,OAAO;QACN,IAAI;QACJ,IAAI;QACJ,uEAAuE;QACvE,qEAAqE;QACrE,MAAM,EAAE,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG;QACnE,IAAI,EAAE,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS;QAC/C,IAAI;KACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { type ILogger, type INotificationProvider, type OutboundMessage, type SendResult } from '@selvajs/platform';
2
+ import { type SmtpEnv } from './smtp-config.js';
3
+ /**
4
+ * SMTP transport. Any server works — a transactional provider, an internal
5
+ * relay, or a mailbox with an app password.
6
+ *
7
+ * Settings are read per send rather than cached, so a restart is the only
8
+ * thing needed to reconfigure and a rotated password takes effect without
9
+ * anyone remembering there is a cache.
10
+ */
11
+ export declare class SmtpNotificationProvider implements INotificationProvider {
12
+ private readonly env;
13
+ readonly name = "SMTP";
14
+ constructor(env: SmtpEnv);
15
+ send(message: OutboundMessage, log?: ILogger): Promise<SendResult>;
16
+ isConfigured(): boolean;
17
+ }
18
+ //# sourceMappingURL=smtp-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smtp-provider.d.ts","sourceRoot":"","sources":["../../src/notifications/smtp-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,OAAO,EACZ,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,UAAU,EACf,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAkB,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhE;;;;;;;GAOG;AACH,qBAAa,wBAAyB,YAAW,qBAAqB;IAGzD,OAAO,CAAC,QAAQ,CAAC,GAAG;IAFhC,QAAQ,CAAC,IAAI,UAAU;gBAEM,GAAG,EAAE,OAAO;IAEnC,IAAI,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,GAAE,OAA0B,GAAG,OAAO,CAAC,UAAU,CAAC;IA6C1F,YAAY,IAAI,OAAO;CAOvB"}