@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,71 @@
1
+ import { NoopLogger } from '@selvajs/platform';
2
+ import { readSmtpConfig } 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 class SmtpNotificationProvider {
12
+ env;
13
+ name = 'SMTP';
14
+ constructor(env) {
15
+ this.env = env;
16
+ }
17
+ async send(message, log = new NoopLogger()) {
18
+ let config;
19
+ try {
20
+ config = readSmtpConfig(this.env);
21
+ }
22
+ catch (err) {
23
+ log.error('SMTP is misconfigured', {
24
+ component: 'SmtpNotificationProvider',
25
+ error: err instanceof Error ? err.message : String(err)
26
+ });
27
+ return { status: 'failed', reason: 'misconfigured' };
28
+ }
29
+ if (!config)
30
+ return { status: 'not-configured' };
31
+ try {
32
+ // Lazy: an instance with no SMTP_HOST never pays to load nodemailer.
33
+ const { createTransport } = await import('nodemailer');
34
+ const transport = createTransport({
35
+ host: config.host,
36
+ port: config.port,
37
+ secure: config.secure,
38
+ auth: config.auth
39
+ });
40
+ await transport.sendMail({
41
+ from: config.from,
42
+ to: message.to,
43
+ subject: message.subject,
44
+ text: message.text,
45
+ html: message.html
46
+ });
47
+ return { status: 'sent' };
48
+ }
49
+ catch (err) {
50
+ const reason = err instanceof Error ? err.message : String(err);
51
+ // Recipient is personal data — logged at warn only, as the minimum
52
+ // needed to diagnose a bounce. Never the subject or body.
53
+ log.warn('Failed to send mail', {
54
+ component: 'SmtpNotificationProvider',
55
+ to: message.to,
56
+ kind: message.kind,
57
+ reason
58
+ });
59
+ return { status: 'failed', reason };
60
+ }
61
+ }
62
+ isConfigured() {
63
+ try {
64
+ return readSmtpConfig(this.env) !== null;
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ }
71
+ //# sourceMappingURL=smtp-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smtp-provider.js","sourceRoot":"","sources":["../../src/notifications/smtp-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,UAAU,EAKV,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAgB,MAAM,kBAAkB,CAAC;AAEhE;;;;;;;GAOG;AACH,MAAM,OAAO,wBAAwB;IAGP;IAFpB,IAAI,GAAG,MAAM,CAAC;IAEvB,YAA6B,GAAY;QAAZ,QAAG,GAAH,GAAG,CAAS;IAAG,CAAC;IAE7C,KAAK,CAAC,IAAI,CAAC,OAAwB,EAAE,MAAe,IAAI,UAAU,EAAE;QACnE,IAAI,MAAM,CAAC;QACX,IAAI,CAAC;YACJ,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACnC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,GAAG,CAAC,KAAK,CAAC,uBAAuB,EAAE;gBAClC,SAAS,EAAE,0BAA0B;gBACrC,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;aACvD,CAAC,CAAC;YACH,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;QACtD,CAAC;QACD,IAAI,CAAC,MAAM;YAAE,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;QAEjD,IAAI,CAAC;YACJ,qEAAqE;YACrE,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;YACvD,MAAM,SAAS,GAAG,eAAe,CAAC;gBACjC,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,IAAI,EAAE,MAAM,CAAC,IAAI;aACjB,CAAC,CAAC;YAEH,MAAM,SAAS,CAAC,QAAQ,CAAC;gBACxB,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,EAAE,EAAE,OAAO,CAAC,EAAE;gBACd,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,IAAI,EAAE,OAAO,CAAC,IAAI;aAClB,CAAC,CAAC;YACH,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAC3B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAChE,mEAAmE;YACnE,0DAA0D;YAC1D,GAAG,CAAC,IAAI,CAAC,qBAAqB,EAAE;gBAC/B,SAAS,EAAE,0BAA0B;gBACrC,EAAE,EAAE,OAAO,CAAC,EAAE;gBACd,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,MAAM;aACN,CAAC,CAAC;YACH,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QACrC,CAAC;IACF,CAAC;IAED,YAAY;QACX,IAAI,CAAC;YACJ,OAAO,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC;QAC1C,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,KAAK,CAAC;QACd,CAAC;IACF,CAAC;CACD"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Reverse-proxy and body-limit configuration rules, evaluated against a plain
3
+ * env bag. Pure predicates — no filesystem, no network, no process.
4
+ *
5
+ * These settings are read by adapter-node, not by Selva, so getting them wrong
6
+ * is invisible: nothing throws, nothing logs, and the deployment serves traffic
7
+ * while a whole class of requests fails. `selva doctor` reports them on the
8
+ * host; the admin health panel runs the same rules so an operator who never
9
+ * opens a shell still sees them.
10
+ *
11
+ * `packages/cli/src/checks/config.js` carries a duplicate of every rule here.
12
+ * Duplicated because the CLI is dependency-free by design — it scaffolds the
13
+ * deployment that installs this runtime, so it cannot import from it. A shared
14
+ * fixture table (`ops/__tests__/deployment-config-fixtures.js`) is asserted by
15
+ * both test suites, so a rule changed on one side fails CI on the other.
16
+ */
17
+ /** Neutral severity. Callers map it onto their own presentation. */
18
+ export type ConfigVerdict = 'ok' | 'warn' | 'fail';
19
+ export interface ConfigFinding {
20
+ id: string;
21
+ label: string;
22
+ verdict: ConfigVerdict;
23
+ summary: string;
24
+ /** Concrete next step. Absent when the verdict is `ok`. */
25
+ remediation?: string;
26
+ }
27
+ /** Just the vars these rules read. Values are raw strings, as they come from `.env`. */
28
+ export interface DeploymentEnv {
29
+ ORIGIN?: string;
30
+ ADDRESS_HEADER?: string;
31
+ XFF_DEPTH?: string;
32
+ BODY_SIZE_LIMIT?: string;
33
+ COMPUTE_REQUEST_MAX_BYTES?: string;
34
+ [key: string]: string | undefined;
35
+ }
36
+ /**
37
+ * Parse a `BODY_SIZE_LIMIT` the way adapter-node does, so this agrees with the
38
+ * thing that actually rejects the request.
39
+ *
40
+ * The quirk is load-bearing: adapter-node reads only the LAST character as the
41
+ * suffix, so `60mb` parses as the digits `60m` and happens to mean 60 MB, while
42
+ * `60xy` silently becomes NaN. Returns bytes, or null when the value is one
43
+ * adapter-node can't use.
44
+ */
45
+ export declare function parseBodySizeLimit(raw: string | undefined | null): number | null;
46
+ /**
47
+ * BODY_SIZE_LIMIT has to clear the compute request cap, or adapter-node's global
48
+ * backstop rejects the upload first — with a non-JSON body the app never sees
49
+ * and cannot log. Unset is the dangerous case, not a neutral one: adapter-node
50
+ * falls back to 512 KB, so every upload 413s on a deployment that looks fine.
51
+ */
52
+ export declare function checkBodySizeLimit(env: DeploymentEnv): ConfigFinding;
53
+ /**
54
+ * `ADDRESS_HEADER` / `XFF_DEPTH` are read by adapter-node, not by Selva, and
55
+ * unset they are invisible: nothing fails, nothing logs, and the deployment
56
+ * looks healthy.
57
+ *
58
+ * What actually happens is that `getClientAddress()` returns the socket peer,
59
+ * which behind a reverse proxy is the proxy — `127.0.0.1` for every request
60
+ * from every user. Login rate limiting keys on that, so the whole instance
61
+ * shares one bucket: five failed logins from anywhere return 429 to everyone,
62
+ * and only a *successful* login clears a bucket, which nobody can then reach.
63
+ *
64
+ * `ORIGIN` is the proxy tell. It is only needed when the app is served under a
65
+ * public URL it can't derive from its own socket, so its presence means a proxy
66
+ * is terminating requests. Without it we say nothing: on a directly-reachable
67
+ * app these settings are a footgun, since `X-Forwarded-For` is client-supplied
68
+ * and anyone could then pick their own rate-limit bucket.
69
+ */
70
+ export declare function checkClientAddress(env: DeploymentEnv): ConfigFinding;
71
+ /** Every deployment-config rule, in report order. */
72
+ export declare function checkDeploymentConfig(env: DeploymentEnv): ConfigFinding[];
73
+ //# sourceMappingURL=deploymentConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deploymentConfig.d.ts","sourceRoot":"","sources":["../../src/ops/deploymentConfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,oEAAoE;AACpE,MAAM,MAAM,aAAa,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAEnD,MAAM,WAAW,aAAa;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wFAAwF;AACxF,MAAM,WAAW,aAAa;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,yBAAyB,CAAC,EAAE,MAAM,CAAC;IAKnC,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CAClC;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAUhF;AAOD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,aAAa,GAAG,aAAa,CA0CpE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,aAAa,GAAG,aAAa,CAwDpE;AAED,qDAAqD;AACrD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,aAAa,GAAG,aAAa,EAAE,CAEzE"}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Reverse-proxy and body-limit configuration rules, evaluated against a plain
3
+ * env bag. Pure predicates — no filesystem, no network, no process.
4
+ *
5
+ * These settings are read by adapter-node, not by Selva, so getting them wrong
6
+ * is invisible: nothing throws, nothing logs, and the deployment serves traffic
7
+ * while a whole class of requests fails. `selva doctor` reports them on the
8
+ * host; the admin health panel runs the same rules so an operator who never
9
+ * opens a shell still sees them.
10
+ *
11
+ * `packages/cli/src/checks/config.js` carries a duplicate of every rule here.
12
+ * Duplicated because the CLI is dependency-free by design — it scaffolds the
13
+ * deployment that installs this runtime, so it cannot import from it. A shared
14
+ * fixture table (`ops/__tests__/deployment-config-fixtures.js`) is asserted by
15
+ * both test suites, so a rule changed on one side fails CI on the other.
16
+ */
17
+ /**
18
+ * Parse a `BODY_SIZE_LIMIT` the way adapter-node does, so this agrees with the
19
+ * thing that actually rejects the request.
20
+ *
21
+ * The quirk is load-bearing: adapter-node reads only the LAST character as the
22
+ * suffix, so `60mb` parses as the digits `60m` and happens to mean 60 MB, while
23
+ * `60xy` silently becomes NaN. Returns bytes, or null when the value is one
24
+ * adapter-node can't use.
25
+ */
26
+ export function parseBodySizeLimit(raw) {
27
+ if (raw == null || raw === '')
28
+ return null;
29
+ const value = String(raw).trim();
30
+ if (value === '')
31
+ return null;
32
+ const units = { K: 1024, M: 1024 * 1024, G: 1024 * 1024 * 1024 };
33
+ const multiplier = units[value.at(-1).toUpperCase()] ?? 1;
34
+ const bytes = Number(multiplier !== 1 ? value.slice(0, -1) : value) * multiplier;
35
+ if (!Number.isFinite(bytes) || bytes <= 0)
36
+ return null;
37
+ return bytes;
38
+ }
39
+ /** The app's own default for COMPUTE_REQUEST_MAX_BYTES (compute/limits.ts). */
40
+ const COMPUTE_REQUEST_DEFAULT_BYTES = 256 * 1024 * 1024;
41
+ const asMb = (bytes) => `${Math.round(bytes / (1024 * 1024))} MB`;
42
+ /**
43
+ * BODY_SIZE_LIMIT has to clear the compute request cap, or adapter-node's global
44
+ * backstop rejects the upload first — with a non-JSON body the app never sees
45
+ * and cannot log. Unset is the dangerous case, not a neutral one: adapter-node
46
+ * falls back to 512 KB, so every upload 413s on a deployment that looks fine.
47
+ */
48
+ export function checkBodySizeLimit(env) {
49
+ const base = { id: 'body-size-limit', label: 'Upload size limit' };
50
+ const raw = env.BODY_SIZE_LIMIT;
51
+ const requestCap = Number(env.COMPUTE_REQUEST_MAX_BYTES) || COMPUTE_REQUEST_DEFAULT_BYTES;
52
+ if (raw == null || raw === '') {
53
+ return {
54
+ ...base,
55
+ verdict: 'fail',
56
+ summary: 'BODY_SIZE_LIMIT is unset — adapter-node falls back to 512 KB, so uploads over ' +
57
+ 'that fail with an opaque 413 no app log records.',
58
+ remediation: `Set BODY_SIZE_LIMIT to at least ${asMb(requestCap)} (the compute request cap) and restart.`
59
+ };
60
+ }
61
+ const bytes = parseBodySizeLimit(raw);
62
+ if (bytes === null) {
63
+ return {
64
+ ...base,
65
+ verdict: 'fail',
66
+ summary: `BODY_SIZE_LIMIT="${raw}" — adapter-node cannot parse this and throws on boot.`,
67
+ remediation: 'Use a byte count or a single K/M/G suffix, e.g. "256M".'
68
+ };
69
+ }
70
+ if (bytes < requestCap) {
71
+ return {
72
+ ...base,
73
+ verdict: 'warn',
74
+ summary: `BODY_SIZE_LIMIT=${raw} (${asMb(bytes)}) is below COMPUTE_REQUEST_MAX_BYTES ` +
75
+ `(${asMb(requestCap)}) — the global cap rejects first, making the compute cap dead config.`,
76
+ remediation: `Raise BODY_SIZE_LIMIT to at least ${asMb(requestCap)} and restart.`
77
+ };
78
+ }
79
+ return {
80
+ ...base,
81
+ verdict: 'ok',
82
+ summary: `BODY_SIZE_LIMIT=${raw} clears the compute request cap.`
83
+ };
84
+ }
85
+ /**
86
+ * `ADDRESS_HEADER` / `XFF_DEPTH` are read by adapter-node, not by Selva, and
87
+ * unset they are invisible: nothing fails, nothing logs, and the deployment
88
+ * looks healthy.
89
+ *
90
+ * What actually happens is that `getClientAddress()` returns the socket peer,
91
+ * which behind a reverse proxy is the proxy — `127.0.0.1` for every request
92
+ * from every user. Login rate limiting keys on that, so the whole instance
93
+ * shares one bucket: five failed logins from anywhere return 429 to everyone,
94
+ * and only a *successful* login clears a bucket, which nobody can then reach.
95
+ *
96
+ * `ORIGIN` is the proxy tell. It is only needed when the app is served under a
97
+ * public URL it can't derive from its own socket, so its presence means a proxy
98
+ * is terminating requests. Without it we say nothing: on a directly-reachable
99
+ * app these settings are a footgun, since `X-Forwarded-For` is client-supplied
100
+ * and anyone could then pick their own rate-limit bucket.
101
+ */
102
+ export function checkClientAddress(env) {
103
+ const base = { id: 'client-address', label: 'Client IP behind proxy' };
104
+ const header = env.ADDRESS_HEADER;
105
+ const depth = env.XFF_DEPTH;
106
+ if (!env.ORIGIN) {
107
+ return {
108
+ ...base,
109
+ verdict: 'ok',
110
+ summary: 'ADDRESS_HEADER is not needed — ORIGIN is unset, so no reverse proxy is in play.'
111
+ };
112
+ }
113
+ if (!header) {
114
+ return {
115
+ ...base,
116
+ verdict: 'fail',
117
+ summary: 'ADDRESS_HEADER is unset while ORIGIN is set — every request looks like it came from ' +
118
+ 'the proxy, so all users share ONE login rate-limit bucket. Five failed logins from ' +
119
+ 'anyone locks out the whole instance for 15 minutes.',
120
+ remediation: 'Set ADDRESS_HEADER=X-Forwarded-For and XFF_DEPTH=<number of proxies>, then restart. ' +
121
+ 'To count your proxies, see docs/self-hosting/deployment/reverse-proxy.md.'
122
+ };
123
+ }
124
+ if (header.toLowerCase() === 'x-forwarded-for' && !depth) {
125
+ return {
126
+ ...base,
127
+ verdict: 'warn',
128
+ summary: `ADDRESS_HEADER=${header} but XFF_DEPTH is unset. X-Forwarded-For is a ` +
129
+ 'client-appendable list, so a caller can prepend fake entries — adapter-node needs ' +
130
+ 'the proxy count to know which entry is real.',
131
+ remediation: 'Set XFF_DEPTH=1 for a single reverse proxy, 2 if a CDN or load balancer sits in ' +
132
+ 'front of it. Unsure? docs/self-hosting/deployment/reverse-proxy.md shows how to ' +
133
+ 'read the header the app actually receives.'
134
+ };
135
+ }
136
+ if (depth !== undefined && !/^[1-9][0-9]*$/.test(depth)) {
137
+ return {
138
+ ...base,
139
+ verdict: 'fail',
140
+ summary: `XFF_DEPTH="${depth}" is not a positive integer.`,
141
+ remediation: 'It counts proxies from the outside in — use 1 for a single reverse proxy.'
142
+ };
143
+ }
144
+ return {
145
+ ...base,
146
+ verdict: 'ok',
147
+ summary: `ADDRESS_HEADER=${header}${depth ? ` (XFF_DEPTH=${depth})` : ''}.`
148
+ };
149
+ }
150
+ /** Every deployment-config rule, in report order. */
151
+ export function checkDeploymentConfig(env) {
152
+ return [checkClientAddress(env), checkBodySizeLimit(env)];
153
+ }
154
+ //# sourceMappingURL=deploymentConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deploymentConfig.js","sourceRoot":"","sources":["../../src/ops/deploymentConfig.ts"],"names":[],"mappings":"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"}
@@ -1,2 +1,3 @@
1
1
  export { parseSemver, isNewer, compareCore, satisfiesRange, type ReleaseChannel } from './semver.js';
2
+ export { checkClientAddress, checkBodySizeLimit, checkDeploymentConfig, parseBodySizeLimit, type ConfigFinding, type ConfigVerdict, type DeploymentEnv } from './deploymentConfig.js';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ops/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACN,WAAW,EACX,OAAO,EACP,WAAW,EACX,cAAc,EACd,KAAK,cAAc,EACnB,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ops/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACN,WAAW,EACX,OAAO,EACP,WAAW,EACX,cAAc,EACd,KAAK,cAAc,EACnB,MAAM,aAAa,CAAC;AAErB,OAAO,EACN,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EAClB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,MAAM,uBAAuB,CAAC"}
package/dist/ops/index.js CHANGED
@@ -1,4 +1,5 @@
1
- // Deployment/ops helpers — channel-aware semver comparison and engines.node
2
- // range checks.
1
+ // Deployment/ops helpers — channel-aware semver comparison, engines.node range
2
+ // checks, and the reverse-proxy/body-limit configuration rules.
3
3
  export { parseSemver, isNewer, compareCore, satisfiesRange } from './semver.js';
4
+ export { checkClientAddress, checkBodySizeLimit, checkDeploymentConfig, parseBodySizeLimit } from './deploymentConfig.js';
4
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ops/index.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,gBAAgB;AAEhB,OAAO,EACN,WAAW,EACX,OAAO,EACP,WAAW,EACX,cAAc,EAEd,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ops/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,gEAAgE;AAEhE,OAAO,EACN,WAAW,EACX,OAAO,EACP,WAAW,EACX,cAAc,EAEd,MAAM,aAAa,CAAC;AAErB,OAAO,EACN,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EAIlB,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { OrgAssetService } from './org-asset-service.js';
2
+ export { listAllOrgMembers } from './list-all-members.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/organizations/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,5 @@
1
+ // Org-level domain services — writes that span more than one provider and so
2
+ // belong to neither store alone.
3
+ export { OrgAssetService } from './org-asset-service.js';
4
+ export { listAllOrgMembers } from './list-all-members.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/organizations/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,iCAAiC;AAEjC,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { ILogger, IOrgStore, OrgMember } from '@selvajs/platform';
2
+ /**
3
+ * Drain every membership page for an org. The admin users pages merge
4
+ * per-user org permissions into these rows; a truncated list would render
5
+ * those users as permissionless, and the permission-toggle UI builds its
6
+ * PATCH payload from the displayed state — so truncation here could silently
7
+ * wipe a user's org permissions on the next edit.
8
+ */
9
+ export declare function listAllOrgMembers(orgs: IOrgStore, orgId: string, log?: ILogger): Promise<OrgMember[]>;
10
+ //# sourceMappingURL=list-all-members.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-all-members.d.ts","sourceRoot":"","sources":["../../src/organizations/list-all-members.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAQvE;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACtC,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,MAAM,EACb,GAAG,CAAC,EAAE,OAAO,GACX,OAAO,CAAC,SAAS,EAAE,CAAC,CAqBtB"}
@@ -0,0 +1,36 @@
1
+ import { SYSTEM_CONTEXT } from '@selvajs/platform';
2
+ const PAGE_LIMIT = 200;
3
+ // Runaway guard against an adapter that returns a non-advancing cursor —
4
+ // 100 pages = 20k members, far past any real org on this product.
5
+ const MAX_PAGES = 100;
6
+ /**
7
+ * Drain every membership page for an org. The admin users pages merge
8
+ * per-user org permissions into these rows; a truncated list would render
9
+ * those users as permissionless, and the permission-toggle UI builds its
10
+ * PATCH payload from the displayed state — so truncation here could silently
11
+ * wipe a user's org permissions on the next edit.
12
+ */
13
+ export async function listAllOrgMembers(orgs, orgId, log) {
14
+ const members = [];
15
+ let cursor;
16
+ for (let page = 0; page < MAX_PAGES; page++) {
17
+ const result = await orgs.listOrgMembers(SYSTEM_CONTEXT, orgId, {
18
+ limit: PAGE_LIMIT,
19
+ cursor
20
+ });
21
+ members.push(...result.items);
22
+ cursor = result.nextCursor;
23
+ if (!cursor)
24
+ return members;
25
+ }
26
+ // Injected rather than resolved: hitting the cap is the one outcome a caller
27
+ // must be able to see, and a package that reached for a host's logger could
28
+ // not be used by a host that has a different one.
29
+ log?.warn('listAllOrgMembers hit the page cap — member list may be incomplete', {
30
+ component: 'admin',
31
+ orgId,
32
+ pages: MAX_PAGES
33
+ });
34
+ return members;
35
+ }
36
+ //# sourceMappingURL=list-all-members.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-all-members.js","sourceRoot":"","sources":["../../src/organizations/list-all-members.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,MAAM,UAAU,GAAG,GAAG,CAAC;AACvB,yEAAyE;AACzE,kEAAkE;AAClE,MAAM,SAAS,GAAG,GAAG,CAAC;AAEtB;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACtC,IAAe,EACf,KAAa,EACb,GAAa;IAEb,MAAM,OAAO,GAAgB,EAAE,CAAC;IAChC,IAAI,MAA0B,CAAC;IAC/B,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,cAAc,EAAE,KAAK,EAAE;YAC/D,KAAK,EAAE,UAAU;YACjB,MAAM;SACN,CAAC,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QAC9B,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC;QAC3B,IAAI,CAAC,MAAM;YAAE,OAAO,OAAO,CAAC;IAC7B,CAAC;IACD,6EAA6E;IAC7E,4EAA4E;IAC5E,kDAAkD;IAClD,GAAG,EAAE,IAAI,CAAC,oEAAoE,EAAE;QAC/E,SAAS,EAAE,OAAO;QAClB,KAAK;QACL,KAAK,EAAE,SAAS;KAChB,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AAChB,CAAC"}
@@ -0,0 +1,39 @@
1
+ import type { IOrgStore, IStorageProvider, OrgAssetKind, RequestContext } from '@selvajs/platform';
2
+ /**
3
+ * Orchestrates org-asset writes that span IStorageProvider + IOrgStore — one
4
+ * service for every asset kind (logo, favicon, …).
5
+ *
6
+ * Every upload is rasterized to WebP by the shared transcoder inside
7
+ * `IStorageProvider.put`, so the stored blob is always
8
+ * `orgs/{id}/branding/{kind}.webp` (the `branding/` segment marks the public
9
+ * asset class — see `classifyAssetPath`). SVG inputs are flattened to a
10
+ * 1200px raster — no vector blob is persisted, which keeps the served bytes
11
+ * XSS-free without a sanitizer. Re-uploads overwrite in place. The org's
12
+ * `assets` map is the source of truth for which kinds exist and their public
13
+ * URLs; we read-merge-write it so concurrent edits to *different* kinds don't
14
+ * clobber each other within a kind.
15
+ *
16
+ * Re-uploads overwrite the same path, so the stored URL would be identical
17
+ * across replaces and a cached browser/CDN would keep serving the old bytes.
18
+ * The content-derived `?v=` token (`withCacheBust`) gives a *different* image a
19
+ * *different* URL, while an identical re-upload keeps its URL and stays cached.
20
+ */
21
+ export declare class OrgAssetService {
22
+ private orgs;
23
+ private storage;
24
+ constructor(orgs: IOrgStore, storage: IStorageProvider);
25
+ /**
26
+ * Store `kind`'s blob and point the org's `assets[kind]` at its public URL.
27
+ * `contentType` must be the *original* upload type (e.g. `image/svg+xml`) so
28
+ * the transcoder recognizes the bytes as an image. Returns the public URL.
29
+ */
30
+ saveAsset(ctx: RequestContext, orgId: string, kind: OrgAssetKind, data: Uint8Array, contentType: string): Promise<string>;
31
+ /** Remove `kind`'s blob and drop it from the org's `assets` map. No-op-safe. */
32
+ removeAsset(ctx: RequestContext, orgId: string, kind: OrgAssetKind): Promise<void>;
33
+ /**
34
+ * Read the org's current `assets` and return a copy with `kind` set to `url`
35
+ * (or removed when `url` is undefined). Keeps the other kinds intact.
36
+ */
37
+ private mergedAssets;
38
+ }
39
+ //# sourceMappingURL=org-asset-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"org-asset-service.d.ts","sourceRoot":"","sources":["../../src/organizations/org-asset-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGnG;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,eAAe;IAE1B,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,OAAO;gBADP,IAAI,EAAE,SAAS,EACf,OAAO,EAAE,gBAAgB;IAGlC;;;;OAIG;IACG,SAAS,CACd,GAAG,EAAE,cAAc,EACnB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,YAAY,EAClB,IAAI,EAAE,UAAU,EAChB,WAAW,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC;IAelB,gFAAgF;IAC1E,WAAW,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAOxF;;;OAGG;YACW,YAAY;CAa1B"}
@@ -0,0 +1,71 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { orgPaths, ProviderError, withCacheBust } from '@selvajs/platform';
3
+ /**
4
+ * Orchestrates org-asset writes that span IStorageProvider + IOrgStore — one
5
+ * service for every asset kind (logo, favicon, …).
6
+ *
7
+ * Every upload is rasterized to WebP by the shared transcoder inside
8
+ * `IStorageProvider.put`, so the stored blob is always
9
+ * `orgs/{id}/branding/{kind}.webp` (the `branding/` segment marks the public
10
+ * asset class — see `classifyAssetPath`). SVG inputs are flattened to a
11
+ * 1200px raster — no vector blob is persisted, which keeps the served bytes
12
+ * XSS-free without a sanitizer. Re-uploads overwrite in place. The org's
13
+ * `assets` map is the source of truth for which kinds exist and their public
14
+ * URLs; we read-merge-write it so concurrent edits to *different* kinds don't
15
+ * clobber each other within a kind.
16
+ *
17
+ * Re-uploads overwrite the same path, so the stored URL would be identical
18
+ * across replaces and a cached browser/CDN would keep serving the old bytes.
19
+ * The content-derived `?v=` token (`withCacheBust`) gives a *different* image a
20
+ * *different* URL, while an identical re-upload keeps its URL and stays cached.
21
+ */
22
+ export class OrgAssetService {
23
+ orgs;
24
+ storage;
25
+ constructor(orgs, storage) {
26
+ this.orgs = orgs;
27
+ this.storage = storage;
28
+ }
29
+ /**
30
+ * Store `kind`'s blob and point the org's `assets[kind]` at its public URL.
31
+ * `contentType` must be the *original* upload type (e.g. `image/svg+xml`) so
32
+ * the transcoder recognizes the bytes as an image. Returns the public URL.
33
+ */
34
+ async saveAsset(ctx, orgId, kind, data, contentType) {
35
+ const path = orgPaths.asset(orgId, kind);
36
+ await this.storage.put(path, data, contentType);
37
+ // Content-derived token, not a timestamp: an identical re-upload to the
38
+ // same path keeps the same URL and stays cached; a changed image gets a
39
+ // new URL and refetches immediately. 8 hex chars is enough to distinguish
40
+ // versions without bloating the URL.
41
+ const token = createHash('sha256').update(data).digest('hex').slice(0, 8);
42
+ const url = withCacheBust(this.storage.getPublicUrl(path), token);
43
+ await this.orgs.updateOrg(ctx, orgId, {
44
+ assets: await this.mergedAssets(ctx, orgId, kind, url)
45
+ });
46
+ return url;
47
+ }
48
+ /** Remove `kind`'s blob and drop it from the org's `assets` map. No-op-safe. */
49
+ async removeAsset(ctx, orgId, kind) {
50
+ await this.storage.delete(orgPaths.asset(orgId, kind));
51
+ await this.orgs.updateOrg(ctx, orgId, {
52
+ assets: await this.mergedAssets(ctx, orgId, kind, undefined)
53
+ });
54
+ }
55
+ /**
56
+ * Read the org's current `assets` and return a copy with `kind` set to `url`
57
+ * (or removed when `url` is undefined). Keeps the other kinds intact.
58
+ */
59
+ async mergedAssets(ctx, orgId, kind, url) {
60
+ const org = await this.orgs.getOrg(ctx, orgId);
61
+ if (!org)
62
+ throw new ProviderError(`Org '${orgId}' not found`, 404);
63
+ const next = { ...(org.assets ?? {}) };
64
+ if (url === undefined)
65
+ delete next[kind];
66
+ else
67
+ next[kind] = url;
68
+ return next;
69
+ }
70
+ }
71
+ //# sourceMappingURL=org-asset-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"org-asset-service.js","sourceRoot":"","sources":["../../src/organizations/org-asset-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAE3E;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,OAAO,eAAe;IAElB;IACA;IAFT,YACS,IAAe,EACf,OAAyB;QADzB,SAAI,GAAJ,IAAI,CAAW;QACf,YAAO,GAAP,OAAO,CAAkB;IAC/B,CAAC;IAEJ;;;;OAIG;IACH,KAAK,CAAC,SAAS,CACd,GAAmB,EACnB,KAAa,EACb,IAAkB,EAClB,IAAgB,EAChB,WAAmB;QAEnB,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACzC,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;QAChD,wEAAwE;QACxE,wEAAwE;QACxE,0EAA0E;QAC1E,qCAAqC;QACrC,MAAM,KAAK,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1E,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;QAClE,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,EAAE;YACrC,MAAM,EAAE,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC;SACtD,CAAC,CAAC;QACH,OAAO,GAAG,CAAC;IACZ,CAAC;IAED,gFAAgF;IAChF,KAAK,CAAC,WAAW,CAAC,GAAmB,EAAE,KAAa,EAAE,IAAkB;QACvE,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;QACvD,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,EAAE;YACrC,MAAM,EAAE,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,CAAC;SAC5D,CAAC,CAAC;IACJ,CAAC;IAED;;;OAGG;IACK,KAAK,CAAC,YAAY,CACzB,GAAmB,EACnB,KAAa,EACb,IAAkB,EAClB,GAAuB;QAEvB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC/C,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,aAAa,CAAC,QAAQ,KAAK,aAAa,EAAE,GAAG,CAAC,CAAC;QACnE,MAAM,IAAI,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,EAA4B,CAAC;QACjE,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC;;YACpC,IAAI,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC;QACtB,OAAO,IAAI,CAAC;IACb,CAAC;CACD"}
@@ -0,0 +1,30 @@
1
+ import type { Project, RequestContext } from '@selvajs/platform';
2
+ /**
3
+ * Fields the caller decides. `slug`, `createdAt`, `updatedAt` and `deletedAt`
4
+ * are derived here so the two create routes cannot disagree about them.
5
+ */
6
+ export type ProjectDraft = Omit<Project, 'slug' | 'createdAt' | 'updatedAt' | 'deletedAt'>;
7
+ export interface CreateProjectOptions {
8
+ /** Context the store writes under — `SYSTEM_CONTEXT` for the admin route. */
9
+ writeCtx: RequestContext;
10
+ /** Slug stem when the name slugifies to nothing. */
11
+ fallbackSlug: string;
12
+ /** Spliced into the 409 on a duplicate name. */
13
+ conflictScope: string;
14
+ }
15
+ /**
16
+ * Create a project, retrying on slug collision.
17
+ *
18
+ * The retry is not a nicety: the caller may not be able to *see* a colliding
19
+ * project under RLS, so a pre-flight `getProjectBySlug` cannot decide it — the
20
+ * unique index is the only source of truth.
21
+ *
22
+ * `/api/v1/projects` and `/api/admin/projects` both create projects and both
23
+ * carried a copy of this loop. The copies had already drifted on three points
24
+ * (error re-throw, slug stem, flag validation), which is what makes this the
25
+ * one place in the access surface where duplication caused real divergence.
26
+ */
27
+ export declare function createProjectWithUniqueSlug(store: {
28
+ createProject(ctx: RequestContext, project: Project): Promise<unknown>;
29
+ }, draft: ProjectDraft, { writeCtx, fallbackSlug, conflictScope }: CreateProjectOptions): Promise<Project>;
30
+ //# sourceMappingURL=create-project.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-project.d.ts","sourceRoot":"","sources":["../../src/projects/create-project.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAyBjE;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC,CAAC;AAE3F,MAAM,WAAW,oBAAoB;IACpC,6EAA6E;IAC7E,QAAQ,EAAE,cAAc,CAAC;IACzB,oDAAoD;IACpD,YAAY,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,aAAa,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,2BAA2B,CAChD,KAAK,EAAE;IACN,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvE,EACD,KAAK,EAAE,YAAY,EACnB,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,EAAE,oBAAoB,GAC7D,OAAO,CAAC,OAAO,CAAC,CAmClB"}
@@ -0,0 +1,56 @@
1
+ import { ProviderError, slugify } from '@selvajs/platform';
2
+ import { apiError, ApiErrorCode } from '../api/errors.js';
3
+ const MAX_SLUG_ATTEMPTS = 25;
4
+ // Both predicates match a Postgres constraint name inside an error message.
5
+ // That is fragile enough that it must exist once: a renamed constraint breaks
6
+ // silently — the retry loop stops retrying and a collision surfaces as a 500.
7
+ function isSlugConflict(err) {
8
+ return (err instanceof ProviderError &&
9
+ err.statusCode === 409 &&
10
+ /projects_org_id_slug_key/.test(err.message));
11
+ }
12
+ function isNameConflict(err) {
13
+ return (err instanceof ProviderError &&
14
+ err.statusCode === 409 &&
15
+ /projects_org_name_unique/.test(err.message));
16
+ }
17
+ /**
18
+ * Create a project, retrying on slug collision.
19
+ *
20
+ * The retry is not a nicety: the caller may not be able to *see* a colliding
21
+ * project under RLS, so a pre-flight `getProjectBySlug` cannot decide it — the
22
+ * unique index is the only source of truth.
23
+ *
24
+ * `/api/v1/projects` and `/api/admin/projects` both create projects and both
25
+ * carried a copy of this loop. The copies had already drifted on three points
26
+ * (error re-throw, slug stem, flag validation), which is what makes this the
27
+ * one place in the access surface where duplication caused real divergence.
28
+ */
29
+ export async function createProjectWithUniqueSlug(store, draft, { writeCtx, fallbackSlug, conflictScope }) {
30
+ const baseSlug = slugify(draft.name) || fallbackSlug;
31
+ const now = new Date().toISOString();
32
+ for (let attempt = 0; attempt < MAX_SLUG_ATTEMPTS; attempt++) {
33
+ const project = {
34
+ ...draft,
35
+ slug: attempt === 0 ? baseSlug : `${baseSlug}-${attempt + 1}`,
36
+ createdAt: now,
37
+ updatedAt: now,
38
+ deletedAt: null
39
+ };
40
+ try {
41
+ await store.createProject(writeCtx, project);
42
+ return project;
43
+ }
44
+ catch (err) {
45
+ if (isNameConflict(err)) {
46
+ apiError(409, ApiErrorCode.CONFLICT, `A project with that name already exists in ${conflictScope}.`);
47
+ }
48
+ // A slug clash is the one error worth another attempt; everything else
49
+ // leaves the loop for the route's error wrapper to map.
50
+ if (!isSlugConflict(err))
51
+ throw err;
52
+ }
53
+ }
54
+ apiError(409, ApiErrorCode.CONFLICT, 'Could not pick a unique project slug after several attempts.');
55
+ }
56
+ //# sourceMappingURL=create-project.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-project.js","sourceRoot":"","sources":["../../src/projects/create-project.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAE1D,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAE7B,4EAA4E;AAC5E,8EAA8E;AAC9E,8EAA8E;AAC9E,SAAS,cAAc,CAAC,GAAY;IACnC,OAAO,CACN,GAAG,YAAY,aAAa;QAC5B,GAAG,CAAC,UAAU,KAAK,GAAG;QACtB,0BAA0B,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAC5C,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,GAAY;IACnC,OAAO,CACN,GAAG,YAAY,aAAa;QAC5B,GAAG,CAAC,UAAU,KAAK,GAAG;QACtB,0BAA0B,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAC5C,CAAC;AACH,CAAC;AAiBD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAChD,KAEC,EACD,KAAmB,EACnB,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAwB;IAE/D,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,YAAY,CAAC;IACrD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAErC,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,iBAAiB,EAAE,OAAO,EAAE,EAAE,CAAC;QAC9D,MAAM,OAAO,GAAY;YACxB,GAAG,KAAK;YACR,IAAI,EAAE,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,IAAI,OAAO,GAAG,CAAC,EAAE;YAC7D,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,IAAI;SACf,CAAC;QAEF,IAAI,CAAC;YACJ,MAAM,KAAK,CAAC,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YAC7C,OAAO,OAAO,CAAC;QAChB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,IAAI,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC;gBACzB,QAAQ,CACP,GAAG,EACH,YAAY,CAAC,QAAQ,EACrB,8CAA8C,aAAa,GAAG,CAC9D,CAAC;YACH,CAAC;YACD,uEAAuE;YACvE,wDAAwD;YACxD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC;gBAAE,MAAM,GAAG,CAAC;QACrC,CAAC;IACF,CAAC;IAED,QAAQ,CACP,GAAG,EACH,YAAY,CAAC,QAAQ,EACrB,8DAA8D,CAC9D,CAAC;AACH,CAAC"}