@cat-factory/server 0.252.0 → 0.253.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 (45) hide show
  1. package/dist/agents/harnessContract.d.ts +56 -0
  2. package/dist/agents/harnessContract.d.ts.map +1 -0
  3. package/dist/agents/harnessContract.js +86 -0
  4. package/dist/agents/harnessContract.js.map +1 -0
  5. package/dist/agents/jobBody.d.ts.map +1 -1
  6. package/dist/agents/jobBody.js +1 -15
  7. package/dist/agents/jobBody.js.map +1 -1
  8. package/dist/app.d.ts.map +1 -1
  9. package/dist/app.js +6 -0
  10. package/dist/app.js.map +1 -1
  11. package/dist/events/InAppNotificationChannel.d.ts +5 -0
  12. package/dist/events/InAppNotificationChannel.d.ts.map +1 -1
  13. package/dist/events/InAppNotificationChannel.js +5 -0
  14. package/dist/events/InAppNotificationChannel.js.map +1 -1
  15. package/dist/github/ProviderRoutingGitHubClient.d.ts +15 -68
  16. package/dist/github/ProviderRoutingGitHubClient.d.ts.map +1 -1
  17. package/dist/github/ProviderRoutingGitHubClient.js +150 -142
  18. package/dist/github/ProviderRoutingGitHubClient.js.map +1 -1
  19. package/dist/index.d.ts +4 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +8 -2
  22. package/dist/index.js.map +1 -1
  23. package/dist/modules/notifications/NotificationRelayController.d.ts.map +1 -1
  24. package/dist/modules/notifications/NotificationRelayController.js +14 -1
  25. package/dist/modules/notifications/NotificationRelayController.js.map +1 -1
  26. package/dist/modules/notifications/NotificationSettingsController.d.ts +17 -0
  27. package/dist/modules/notifications/NotificationSettingsController.d.ts.map +1 -0
  28. package/dist/modules/notifications/NotificationSettingsController.js +46 -0
  29. package/dist/modules/notifications/NotificationSettingsController.js.map +1 -0
  30. package/dist/notifications/machineNotifications.d.ts +13 -4
  31. package/dist/notifications/machineNotifications.d.ts.map +1 -1
  32. package/dist/notifications/machineNotifications.js +4 -3
  33. package/dist/notifications/machineNotifications.js.map +1 -1
  34. package/dist/notifications/notificationDelivery.d.ts +36 -0
  35. package/dist/notifications/notificationDelivery.d.ts.map +1 -0
  36. package/dist/notifications/notificationDelivery.js +43 -0
  37. package/dist/notifications/notificationDelivery.js.map +1 -0
  38. package/dist/persistence/binaryArtifactStore.d.ts +21 -3
  39. package/dist/persistence/binaryArtifactStore.d.ts.map +1 -1
  40. package/dist/persistence/binaryArtifactStore.js +106 -3
  41. package/dist/persistence/binaryArtifactStore.js.map +1 -1
  42. package/dist/persistence/rpc-allowlist.d.ts.map +1 -1
  43. package/dist/persistence/rpc-allowlist.js +8 -0
  44. package/dist/persistence/rpc-allowlist.js.map +1 -1
  45. package/package.json +7 -7
@@ -0,0 +1,46 @@
1
+ import { getNotificationSettingsContract, updateNotificationSettingsContract, } from '@cat-factory/contracts';
2
+ import { buildHonoRoute } from '@toad-contracts/hono';
3
+ import { Hono } from 'hono';
4
+ import { requireCapability } from '../../http/guards.js';
5
+ import { param } from '../../http/params.js';
6
+ import { mountWorkspacePermission } from '../../http/workspaceAccess.js';
7
+ /**
8
+ * Resolve the notification manager, or refuse with a 503 naming what isn't wired. It has
9
+ * its OWN accessor rather than borrowing the notifications module's: the routing store is
10
+ * optional within a wired notifications module, so a message about notifications being
11
+ * unconfigured would name a subsystem the operator has already wired.
12
+ */
13
+ function requireNotificationSettings(c) {
14
+ return requireCapability(c.get('container').notifications?.settingsService, 'The notification manager is not configured');
15
+ }
16
+ /**
17
+ * The notification manager: which notification types this board delivers on which channel.
18
+ *
19
+ * Its own controller rather than routes on {@link notificationController}, because the tiers
20
+ * differ: acting on a card is a MEMBER's everyday work, while re-routing what the whole board
21
+ * is told is workspace configuration. A controller gates one permission over its own prefixes,
22
+ * so mixing the two would either under-gate the settings or lock members out of their inbox.
23
+ *
24
+ * The READ is left at the member tier on purpose: the settings surface renders the resolved
25
+ * routing, and it carries no credential or endpoint — only which types go where.
26
+ *
27
+ * Mounted under `/workspaces/:workspaceId`.
28
+ */
29
+ export function notificationSettingsController() {
30
+ const app = new Hono();
31
+ mountWorkspacePermission(app, 'settings.manage', ['/notification-settings']);
32
+ buildHonoRoute(app, getNotificationSettingsContract, async (c) => {
33
+ const settings = requireNotificationSettings(c);
34
+ return c.json(await settings.get(param(c, 'workspaceId')), 200);
35
+ });
36
+ // A full replace of the override matrix, like the Slack routing write: the settings surface
37
+ // always sends the whole thing, and a merge would make "turn this cell back to its default"
38
+ // unexpressible.
39
+ buildHonoRoute(app, updateNotificationSettingsContract, async (c) => {
40
+ const settings = requireNotificationSettings(c);
41
+ const { matrix } = c.req.valid('json');
42
+ return c.json(await settings.update(param(c, 'workspaceId'), matrix), 200);
43
+ });
44
+ return app;
45
+ }
46
+ //# sourceMappingURL=NotificationSettingsController.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NotificationSettingsController.js","sourceRoot":"","sources":["../../../src/modules/notifications/NotificationSettingsController.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,+BAA+B,EAC/B,kCAAkC,GACnC,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAG3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAC5C,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AAExE;;;;;GAKG;AACH,SAAS,2BAA2B,CAAmB,CAAa;IAClE,OAAO,iBAAiB,CACtB,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,aAAa,EAAE,eAAe,EACjD,4CAA4C,CAC7C,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,8BAA8B;IAC5C,MAAM,GAAG,GAAG,IAAI,IAAI,EAAU,CAAA;IAC9B,wBAAwB,CAAC,GAAG,EAAE,iBAAiB,EAAE,CAAC,wBAAwB,CAAC,CAAC,CAAA;IAE5E,cAAc,CAAC,GAAG,EAAE,+BAA+B,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAC/D,MAAM,QAAQ,GAAG,2BAA2B,CAAC,CAAC,CAAC,CAAA;QAC/C,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;IACjE,CAAC,CAAC,CAAA;IAEF,4FAA4F;IAC5F,4FAA4F;IAC5F,iBAAiB;IACjB,cAAc,CAAC,GAAG,EAAE,kCAAkC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAClE,MAAM,QAAQ,GAAG,2BAA2B,CAAC,CAAC,CAAC,CAAA;QAC/C,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QACtC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,aAAa,CAAC,EAAE,MAAM,CAAC,EAAE,GAAG,CAAC,CAAA;IAC5E,CAAC,CAAC,CAAA;IAEF,OAAO,GAAG,CAAA;AACZ,CAAC"}
@@ -1,4 +1,4 @@
1
- import type { Notification, NotificationChannel } from '@cat-factory/kernel';
1
+ import type { Notification, NotificationChannel, NotificationDeliveryReason } from '@cat-factory/kernel';
2
2
  /**
3
3
  * The request a mothership-mode node posts to `POST /internal/notifications/deliver`.
4
4
  *
@@ -6,10 +6,19 @@ import type { Notification, NotificationChannel } from '@cat-factory/kernel';
6
6
  * from its own store and delivers THAT. A compromised node token can therefore only ask the
7
7
  * mothership to re-deliver a notification that already exists in an account it can already reach —
8
8
  * it can never inject forged text into the org's Slack channel.
9
+ *
10
+ * The one thing the row does NOT carry is `reason`: which lifecycle edge this delegation is for.
11
+ * The mothership cannot re-derive it, because the two edges that decide whether its alert
12
+ * transports fire are indistinguishable in the persisted row (an escalation and a fresh raise are
13
+ * both `open`). Left off the wire, every dismissal a laptop made would reach the org's Slack as a
14
+ * fresh "Decision needed". So it is REQUIRED, and a body without it is refused rather than
15
+ * defaulted: guessing `raised` mails a decision already made, guessing `settled` silences the
16
+ * alert the delegation exists to send, and a node one build behind is better off failing loudly.
9
17
  */
10
18
  export interface DelegatedNotificationRequest {
11
19
  workspaceId: string;
12
20
  notificationId: string;
21
+ reason: NotificationDeliveryReason;
13
22
  }
14
23
  /** The client half: asks a mothership to deliver one of its own notification rows. */
15
24
  export interface MachineNotificationClient {
@@ -18,7 +27,7 @@ export interface MachineNotificationClient {
18
27
  * channels. Rejects on a transport/HTTP failure so the caller can surface it; delivery itself is
19
28
  * best-effort on the mothership side (the persisted row is the source of truth).
20
29
  */
21
- deliver(workspaceId: string, notificationId: string): Promise<void>;
30
+ deliver(workspaceId: string, notificationId: string, reason: NotificationDeliveryReason): Promise<void>;
22
31
  }
23
32
  export interface HttpMachineNotificationClientOptions {
24
33
  baseUrl: string;
@@ -40,7 +49,7 @@ export interface HttpMachineNotificationClientOptions {
40
49
  export declare class HttpMachineNotificationClient implements MachineNotificationClient {
41
50
  private readonly opts;
42
51
  constructor(opts: HttpMachineNotificationClientOptions);
43
- deliver(workspaceId: string, notificationId: string): Promise<void>;
52
+ deliver(workspaceId: string, notificationId: string, reason: NotificationDeliveryReason): Promise<void>;
44
53
  }
45
54
  export interface RemoteNotificationChannelOptions {
46
55
  client: MachineNotificationClient;
@@ -65,6 +74,6 @@ export interface RemoteNotificationChannelOptions {
65
74
  export declare class RemoteNotificationChannel implements NotificationChannel {
66
75
  private readonly opts;
67
76
  constructor(opts: RemoteNotificationChannelOptions);
68
- deliver(workspaceId: string, notification: Notification): Promise<void>;
77
+ deliver(workspaceId: string, notification: Notification, reason: NotificationDeliveryReason): Promise<void>;
69
78
  }
70
79
  //# sourceMappingURL=machineNotifications.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"machineNotifications.d.ts","sourceRoot":"","sources":["../../src/notifications/machineNotifications.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAwB5E;;;;;;;GAOG;AACH,MAAM,WAAW,4BAA4B;IAC3C,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,sFAAsF;AACtF,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACpE;AAED,MAAM,WAAW,oCAAoC;IACnD,OAAO,EAAE,MAAM,CAAA;IACf,6FAA6F;IAC7F,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,IAAI,CAAC,CAAA;IACrC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAKD;;;;;GAKG;AACH,qBAAa,6BAA8B,YAAW,yBAAyB;IACjE,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAjC,YAA6B,IAAI,EAAE,oCAAoC,EAAI;IAErE,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBxE;CACF;AAED,MAAM,WAAW,gCAAgC;IAC/C,MAAM,EAAE,yBAAyB,CAAA;IACjC;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CACzF;AAED;;;;;;;GAOG;AACH,qBAAa,yBAA0B,YAAW,mBAAmB;IACvD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAjC,YAA6B,IAAI,EAAE,gCAAgC,EAAI;IAEjE,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAM5E;CACF"}
1
+ {"version":3,"file":"machineNotifications.d.ts","sourceRoot":"","sources":["../../src/notifications/machineNotifications.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,0BAA0B,EAC3B,MAAM,qBAAqB,CAAA;AAwB5B;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,4BAA4B;IAC3C,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,MAAM,EAAE,0BAA0B,CAAA;CACnC;AAED,sFAAsF;AACtF,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,OAAO,CACL,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,0BAA0B,GACjC,OAAO,CAAC,IAAI,CAAC,CAAA;CACjB;AAED,MAAM,WAAW,oCAAoC;IACnD,OAAO,EAAE,MAAM,CAAA;IACf,6FAA6F;IAC7F,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,IAAI,CAAC,CAAA;IACrC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAKD;;;;;GAKG;AACH,qBAAa,6BAA8B,YAAW,yBAAyB;IACjE,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAjC,YAA6B,IAAI,EAAE,oCAAoC,EAAI;IAErE,OAAO,CACX,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,0BAA0B,GACjC,OAAO,CAAC,IAAI,CAAC,CAsBf;CACF;AAED,MAAM,WAAW,gCAAgC;IAC/C,MAAM,EAAE,yBAAyB,CAAA;IACjC;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CACzF;AAED;;;;;;;GAOG;AACH,qBAAa,yBAA0B,YAAW,mBAAmB;IACvD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAjC,YAA6B,IAAI,EAAE,gCAAgC,EAAI;IAEjE,OAAO,CACX,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,YAAY,EAC1B,MAAM,EAAE,0BAA0B,GACjC,OAAO,CAAC,IAAI,CAAC,CAMf;CACF"}
@@ -11,7 +11,7 @@ export class HttpMachineNotificationClient {
11
11
  constructor(opts) {
12
12
  this.opts = opts;
13
13
  }
14
- async deliver(workspaceId, notificationId) {
14
+ async deliver(workspaceId, notificationId, reason) {
15
15
  const token = typeof this.opts.token === 'function' ? this.opts.token() : this.opts.token;
16
16
  // No token yet (a node booted before the mothership login): there is nothing to delegate to,
17
17
  // and a guaranteed-403 round-trip per notification is pure waste. The row is still persisted.
@@ -24,6 +24,7 @@ export class HttpMachineNotificationClient {
24
24
  body: JSON.stringify({
25
25
  workspaceId,
26
26
  notificationId,
27
+ reason,
27
28
  }),
28
29
  signal: AbortSignal.timeout(this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
29
30
  });
@@ -45,9 +46,9 @@ export class RemoteNotificationChannel {
45
46
  constructor(opts) {
46
47
  this.opts = opts;
47
48
  }
48
- async deliver(workspaceId, notification) {
49
+ async deliver(workspaceId, notification, reason) {
49
50
  try {
50
- await this.opts.client.deliver(workspaceId, notification.id);
51
+ await this.opts.client.deliver(workspaceId, notification.id, reason);
51
52
  }
52
53
  catch (error) {
53
54
  this.opts.onError?.(error, { workspaceId, notificationId: notification.id });
@@ -1 +1 @@
1
- {"version":3,"file":"machineNotifications.js","sourceRoot":"","sources":["../../src/notifications/machineNotifications.ts"],"names":[],"mappings":"AA2DA,4FAA4F;AAC5F,MAAM,kBAAkB,GAAG,MAAM,CAAA;AAEjC;;;;;GAKG;AACH,MAAM,OAAO,6BAA6B;IACX,IAAI;IAAjC,YAA6B,IAA0C;oBAA1C,IAAI;IAAyC,CAAC;IAE3E,KAAK,CAAC,OAAO,CAAC,WAAmB,EAAE,cAAsB;QACvD,MAAM,KAAK,GAAG,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAA;QACzF,6FAA6F;QAC7F,8FAA8F;QAC9F,IAAI,CAAC,KAAK;YAAE,OAAM;QAClB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,KAAK,CAAA;QAC9C,MAAM,GAAG,GAAG,MAAM,SAAS,CACzB,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,iCAAiC,EACxE;YACE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE;YACjF,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,WAAW;gBACX,cAAc;aACwB,CAAC;YACzC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,kBAAkB,CAAC;SACvE,CACF,CAAA;QACD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,qDAAqD,GAAG,CAAC,MAAM,EAAE,CAAC,CAAA;QACpF,CAAC;IACH,CAAC;CACF;AAYD;;;;;;;GAOG;AACH,MAAM,OAAO,yBAAyB;IACP,IAAI;IAAjC,YAA6B,IAAsC;oBAAtC,IAAI;IAAqC,CAAC;IAEvE,KAAK,CAAC,OAAO,CAAC,WAAmB,EAAE,YAA0B;QAC3D,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,YAAY,CAAC,EAAE,CAAC,CAAA;QAC9D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,CAAC,EAAE,EAAE,CAAC,CAAA;QAC9E,CAAC;IACH,CAAC;CACF"}
1
+ {"version":3,"file":"machineNotifications.js","sourceRoot":"","sources":["../../src/notifications/machineNotifications.ts"],"names":[],"mappings":"AA4EA,4FAA4F;AAC5F,MAAM,kBAAkB,GAAG,MAAM,CAAA;AAEjC;;;;;GAKG;AACH,MAAM,OAAO,6BAA6B;IACX,IAAI;IAAjC,YAA6B,IAA0C;oBAA1C,IAAI;IAAyC,CAAC;IAE3E,KAAK,CAAC,OAAO,CACX,WAAmB,EACnB,cAAsB,EACtB,MAAkC;QAElC,MAAM,KAAK,GAAG,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAA;QACzF,6FAA6F;QAC7F,8FAA8F;QAC9F,IAAI,CAAC,KAAK;YAAE,OAAM;QAClB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,KAAK,CAAA;QAC9C,MAAM,GAAG,GAAG,MAAM,SAAS,CACzB,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,iCAAiC,EACxE;YACE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE;YACjF,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,WAAW;gBACX,cAAc;gBACd,MAAM;aACgC,CAAC;YACzC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,kBAAkB,CAAC;SACvE,CACF,CAAA;QACD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,qDAAqD,GAAG,CAAC,MAAM,EAAE,CAAC,CAAA;QACpF,CAAC;IACH,CAAC;CACF;AAYD;;;;;;;GAOG;AACH,MAAM,OAAO,yBAAyB;IACP,IAAI;IAAjC,YAA6B,IAAsC;oBAAtC,IAAI;IAAqC,CAAC;IAEvE,KAAK,CAAC,OAAO,CACX,WAAmB,EACnB,YAA0B,EAC1B,MAAkC;QAElC,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,YAAY,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;QACtE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,CAAC,EAAE,EAAE,CAAC,CAAA;QAC9E,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,36 @@
1
+ import type { Clock, EmailSender, MembershipRepository, NotificationChannel, NotificationDeliveryChannel, NotificationSettingsRepository, UserRepository, WorkspaceMemberRepository, WorkspaceRepository } from '@cat-factory/kernel';
2
+ import { NotificationSettingsService } from '@cat-factory/orchestration';
3
+ export interface NotificationDeliveryInput {
4
+ /** The manager's store. Absent ⇒ no routing store; see {@link buildNotificationDelivery}. */
5
+ notificationSettingsRepository: NotificationSettingsRepository;
6
+ clock: Clock;
7
+ workspaceRepository: WorkspaceRepository;
8
+ membershipRepository: MembershipRepository;
9
+ workspaceMemberRepository: WorkspaceMemberRepository;
10
+ userRepository: UserRepository;
11
+ /**
12
+ * The account's configured transactional sender, when the deployment wired the per-account
13
+ * email module (`EmailConnectionService.resolveSender`). Absent ⇒ no email channel is built
14
+ * at all, the standard opt-in pass-through.
15
+ */
16
+ resolveEmailSender?: (accountId: string) => Promise<EmailSender | null>;
17
+ /** The deployment's public SPA base URL, for the "open in cat-factory" deep link. */
18
+ appBaseUrl?: string;
19
+ }
20
+ export interface NotificationDeliverySupport {
21
+ /** The manager: hand it to `CoreDependencies` so the settings API writes the same rows. */
22
+ settingsService: NotificationSettingsService;
23
+ /** Wrap a channel the manager owns, so its deliveries obey the workspace's routing. */
24
+ route(channel: NotificationDeliveryChannel, inner: NotificationChannel): NotificationChannel;
25
+ /** The email transport, or null when the deployment wired no per-account email module. */
26
+ emailChannel: NotificationChannel | null;
27
+ }
28
+ /**
29
+ * Build the notification manager, the per-channel routing gate, and the email channel.
30
+ *
31
+ * The email channel is EXTERNAL in the mothership sense (it sends through the account's
32
+ * sealed provider key), so a facade composes it into its external channel set beside Slack
33
+ * and the webhook, not into the in-app half.
34
+ */
35
+ export declare function buildNotificationDelivery(input: NotificationDeliveryInput): NotificationDeliverySupport;
36
+ //# sourceMappingURL=notificationDelivery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notificationDelivery.d.ts","sourceRoot":"","sources":["../../src/notifications/notificationDelivery.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,KAAK,EACL,WAAW,EACX,oBAAoB,EACpB,mBAAmB,EACnB,2BAA2B,EAC3B,8BAA8B,EAC9B,cAAc,EACd,yBAAyB,EACzB,mBAAmB,EACpB,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAA;AAkBxE,MAAM,WAAW,yBAAyB;IACxC,6FAA6F;IAC7F,8BAA8B,EAAE,8BAA8B,CAAA;IAC9D,KAAK,EAAE,KAAK,CAAA;IACZ,mBAAmB,EAAE,mBAAmB,CAAA;IACxC,oBAAoB,EAAE,oBAAoB,CAAA;IAC1C,yBAAyB,EAAE,yBAAyB,CAAA;IACpD,cAAc,EAAE,cAAc,CAAA;IAC9B;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IACvE,qFAAqF;IACrF,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,2BAA2B;IAC1C,2FAA2F;IAC3F,eAAe,EAAE,2BAA2B,CAAA;IAC5C,uFAAuF;IACvF,KAAK,CAAC,OAAO,EAAE,2BAA2B,EAAE,KAAK,EAAE,mBAAmB,GAAG,mBAAmB,CAAA;IAC5F,0FAA0F;IAC1F,YAAY,EAAE,mBAAmB,GAAG,IAAI,CAAA;CACzC;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,yBAAyB,GAC/B,2BAA2B,CA0C7B"}
@@ -0,0 +1,43 @@
1
+ import { EmailNotificationChannel } from '@cat-factory/integrations';
2
+ import { RoutedNotificationChannel } from '@cat-factory/kernel';
3
+ import { NotificationSettingsService } from '@cat-factory/orchestration';
4
+ import { logger } from '../observability/logger.js';
5
+ /**
6
+ * Build the notification manager, the per-channel routing gate, and the email channel.
7
+ *
8
+ * The email channel is EXTERNAL in the mothership sense (it sends through the account's
9
+ * sealed provider key), so a facade composes it into its external channel set beside Slack
10
+ * and the webhook, not into the in-app half.
11
+ */
12
+ export function buildNotificationDelivery(input) {
13
+ const settingsService = new NotificationSettingsService({
14
+ notificationSettingsRepository: input.notificationSettingsRepository,
15
+ clock: input.clock,
16
+ });
17
+ const route = (channel, inner) => new RoutedNotificationChannel(channel, settingsService, inner, (error, ctx) =>
18
+ // The gate fell back to the shipped default rather than dropping the notification; say
19
+ // so, because a settings store that cannot be read is an outage in its own right.
20
+ logger.warn('notification routing lookup failed; using the default route', {
21
+ err: error instanceof Error ? error.message : String(error),
22
+ ...ctx,
23
+ }));
24
+ const resolveSender = input.resolveEmailSender;
25
+ const emailChannel = resolveSender
26
+ ? route('email', new EmailNotificationChannel({
27
+ workspaceRepository: input.workspaceRepository,
28
+ membershipRepository: input.membershipRepository,
29
+ workspaceMemberRepository: input.workspaceMemberRepository,
30
+ userRepository: input.userRepository,
31
+ resolveSender,
32
+ ...(input.appBaseUrl ? { appBaseUrl: input.appBaseUrl } : {}),
33
+ // Best-effort delivery still surfaces failures (a rejected API key, a bounced
34
+ // address) through the structured logger so a broken sender is diagnosable.
35
+ onError: (error, ctx) => logger.warn('email notification delivery failed', {
36
+ err: error instanceof Error ? error.message : String(error),
37
+ ...ctx,
38
+ }),
39
+ }))
40
+ : null;
41
+ return { settingsService, route, emailChannel };
42
+ }
43
+ //# sourceMappingURL=notificationDelivery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notificationDelivery.js","sourceRoot":"","sources":["../../src/notifications/notificationDelivery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAA;AAYpE,OAAO,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAA;AACxE,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAA;AA4CnD;;;;;;GAMG;AACH,MAAM,UAAU,yBAAyB,CACvC,KAAgC;IAEhC,MAAM,eAAe,GAAG,IAAI,2BAA2B,CAAC;QACtD,8BAA8B,EAAE,KAAK,CAAC,8BAA8B;QACpE,KAAK,EAAE,KAAK,CAAC,KAAK;KACnB,CAAC,CAAA;IAEF,MAAM,KAAK,GAAG,CACZ,OAAoC,EACpC,KAA0B,EACL,EAAE,CACvB,IAAI,yBAAyB,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;IAC5E,uFAAuF;IACvF,kFAAkF;IAClF,MAAM,CAAC,IAAI,CAAC,6DAA6D,EAAE;QACzE,GAAG,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;QAC3D,GAAG,GAAG;KACP,CAAC,CACH,CAAA;IAEH,MAAM,aAAa,GAAG,KAAK,CAAC,kBAAkB,CAAA;IAC9C,MAAM,YAAY,GAAG,aAAa;QAChC,CAAC,CAAC,KAAK,CACH,OAAO,EACP,IAAI,wBAAwB,CAAC;YAC3B,mBAAmB,EAAE,KAAK,CAAC,mBAAmB;YAC9C,oBAAoB,EAAE,KAAK,CAAC,oBAAoB;YAChD,yBAAyB,EAAE,KAAK,CAAC,yBAAyB;YAC1D,cAAc,EAAE,KAAK,CAAC,cAAc;YACpC,aAAa;YACb,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7D,8EAA8E;YAC9E,4EAA4E;YAC5E,OAAO,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CACtB,MAAM,CAAC,IAAI,CAAC,oCAAoC,EAAE;gBAChD,GAAG,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;gBAC3D,GAAG,GAAG;aACP,CAAC;SACL,CAAC,CACH;QACH,CAAC,CAAC,IAAI,CAAA;IAER,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,YAAY,EAAE,CAAA;AACjD,CAAC"}
@@ -1,5 +1,5 @@
1
- import type { ContentStorageBackend, ContentStorageConfig, ContentStorageFsConfig, ContentStorageS3Config, S3CredentialsSecret } from '@cat-factory/contracts';
2
- import type { BinaryArtifactMetadataStore, BinaryArtifactStorageKind, BinaryBlobBackend, Clock, IdGenerator, Logger, ResolveBinaryArtifactStore } from '@cat-factory/kernel';
1
+ import type { ContentStorageBackend, ContentStorageCapability, ContentStorageConfig, ContentStorageFsConfig, ContentStorageS3Config, S3CredentialsSecret } from '@cat-factory/contracts';
2
+ import type { BinaryArtifactMetadataStore, BinaryArtifactStorageKind, BinaryBlobBackend, BinaryStoreRegistry, Clock, IdGenerator, Logger, ResolveBinaryArtifactStore } from '@cat-factory/kernel';
3
3
  /** The non-secret + decrypted content-storage settings the resolver needs (structural). */
4
4
  export interface ContentStorageSettingsResolver {
5
5
  resolve(accountId: string): Promise<{
@@ -19,9 +19,20 @@ export interface BuildBlobBackendOptions {
19
19
  /**
20
20
  * Build the blob backend a runtime can serve for `kind`, or `null` when the runtime does
21
21
  * not support it (e.g. `fs` on Cloudflare, `r2` on Node) — `null` ⇒ storage unavailable.
22
- * `kind` is never `'off'` (the resolver short-circuits that before calling the factory).
22
+ * `kind` is never `'off'` and never `'custom'`: the resolver short-circuits the first and serves
23
+ * the second from the app-owned {@link BinaryStoreRegistry}, which is runtime-neutral, so
24
+ * neither facade's factory has to learn about deployment-registered stores at all.
23
25
  */
24
26
  export type BuildBlobBackend = (kind: BinaryArtifactStorageKind, opts: BuildBlobBackendOptions) => BinaryBlobBackend | null;
27
+ /**
28
+ * Fold the deployment's registered stores into a runtime's declared content-storage capability:
29
+ * the `custom` option appears in the picker exactly when at least one store is registered.
30
+ *
31
+ * Shared rather than done per facade because it is the answer to one question ("what may an
32
+ * account select here"), and a facade that assembled it itself is a facade that can offer
33
+ * `custom` with nothing behind it, or register stores nothing offers.
34
+ */
35
+ export declare function withRegisteredBinaryStores(capability: ContentStorageCapability, registry?: BinaryStoreRegistry): ContentStorageCapability;
25
36
  export interface MakeResolveBinaryArtifactStoreDeps {
26
37
  /**
27
38
  * Resolves an account's decrypted content-storage settings (the AccountSettingsService).
@@ -37,6 +48,13 @@ export interface MakeResolveBinaryArtifactStoreDeps {
37
48
  clock: Clock;
38
49
  /** Builds the blob backend for a resolved backend kind; `null` ⇒ unsupported on this runtime. */
39
50
  buildBlobBackend: BuildBlobBackend;
51
+ /**
52
+ * The deployment's own binary artifact stores, registered in code. Consulted only when an
53
+ * account selects `backend: 'custom'`; absent ⇒ this deployment registers none, and such an
54
+ * account resolves to no storage (named in the log, since the alternative is an account that
55
+ * reads as configured and silently retains nothing).
56
+ */
57
+ binaryStoreRegistry?: BinaryStoreRegistry;
40
58
  /** Backend used when an account has no content-storage config (the runtime default). */
41
59
  defaultBackend: ContentStorageBackend;
42
60
  /** Optional structural logger, forwarded to the composed store to surface partial reclaims. */
@@ -1 +1 @@
1
- {"version":3,"file":"binaryArtifactStore.d.ts","sourceRoot":"","sources":["../../src/persistence/binaryArtifactStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EACrB,oBAAoB,EACpB,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACpB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EACV,2BAA2B,EAC3B,yBAAyB,EAEzB,iBAAiB,EACjB,KAAK,EACL,WAAW,EACX,MAAM,EACN,0BAA0B,EAC3B,MAAM,qBAAqB,CAAA;AAW5B,2FAA2F;AAC3F,MAAM,WAAW,8BAA8B;IAC7C,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAClC,MAAM,EAAE;YAAE,cAAc,CAAC,EAAE,oBAAoB,CAAA;SAAE,CAAA;QACjD,aAAa,CAAC,EAAE,mBAAmB,CAAA;KACpC,CAAC,CAAA;CACH;AAED,+FAA+F;AAC/F,MAAM,WAAW,uBAAuB;IACtC,EAAE,CAAC,EAAE,sBAAsB,CAAA;IAC3B,EAAE,CAAC,EAAE,sBAAsB,CAAA;IAC3B,8EAA8E;IAC9E,aAAa,CAAC,EAAE,mBAAmB,CAAA;CACpC;AAED;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAC7B,IAAI,EAAE,yBAAyB,EAC/B,IAAI,EAAE,uBAAuB,KAC1B,iBAAiB,GAAG,IAAI,CAAA;AAE7B,MAAM,WAAW,kCAAkC;IACjD;;;;OAIG;IACH,eAAe,CAAC,EAAE,8BAA8B,CAAA;IAChD,gFAAgF;IAChF,SAAS,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;IACtE,6FAA6F;IAC7F,QAAQ,EAAE,2BAA2B,CAAA;IACrC,WAAW,EAAE,WAAW,CAAA;IACxB,KAAK,EAAE,KAAK,CAAA;IACZ,iGAAiG;IACjG,gBAAgB,EAAE,gBAAgB,CAAA;IAClC,wFAAwF;IACxF,cAAc,EAAE,qBAAqB,CAAA;IACrC,+FAA+F;IAC/F,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAoBD;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,kCAAkC,GACvC,0BAA0B,CA6C5B"}
1
+ {"version":3,"file":"binaryArtifactStore.d.ts","sourceRoot":"","sources":["../../src/persistence/binaryArtifactStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EACrB,wBAAwB,EACxB,oBAAoB,EAEpB,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACpB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EACV,2BAA2B,EAC3B,yBAAyB,EAEzB,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,EACL,WAAW,EACX,MAAM,EACN,0BAA0B,EAC3B,MAAM,qBAAqB,CAAA;AAW5B,2FAA2F;AAC3F,MAAM,WAAW,8BAA8B;IAC7C,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAClC,MAAM,EAAE;YAAE,cAAc,CAAC,EAAE,oBAAoB,CAAA;SAAE,CAAA;QACjD,aAAa,CAAC,EAAE,mBAAmB,CAAA;KACpC,CAAC,CAAA;CACH;AAED,+FAA+F;AAC/F,MAAM,WAAW,uBAAuB;IACtC,EAAE,CAAC,EAAE,sBAAsB,CAAA;IAC3B,EAAE,CAAC,EAAE,sBAAsB,CAAA;IAC3B,8EAA8E;IAC9E,aAAa,CAAC,EAAE,mBAAmB,CAAA;CACpC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAC7B,IAAI,EAAE,yBAAyB,EAC/B,IAAI,EAAE,uBAAuB,KAC1B,iBAAiB,GAAG,IAAI,CAAA;AAE7B;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,wBAAwB,EACpC,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,wBAAwB,CAS1B;AAED,MAAM,WAAW,kCAAkC;IACjD;;;;OAIG;IACH,eAAe,CAAC,EAAE,8BAA8B,CAAA;IAChD,gFAAgF;IAChF,SAAS,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;IACtE,6FAA6F;IAC7F,QAAQ,EAAE,2BAA2B,CAAA;IACrC,WAAW,EAAE,WAAW,CAAA;IACxB,KAAK,EAAE,KAAK,CAAA;IACZ,iGAAiG;IACjG,gBAAgB,EAAE,gBAAgB,CAAA;IAClC;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,mBAAmB,CAAA;IACzC,wFAAwF;IACxF,cAAc,EAAE,qBAAqB,CAAA;IACrC,+FAA+F;IAC/F,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AA6GD;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,kCAAkC,GACvC,0BAA0B,CAkE5B"}
@@ -1,4 +1,22 @@
1
1
  import { createBinaryArtifactStore } from '@cat-factory/kernel';
2
+ /**
3
+ * Fold the deployment's registered stores into a runtime's declared content-storage capability:
4
+ * the `custom` option appears in the picker exactly when at least one store is registered.
5
+ *
6
+ * Shared rather than done per facade because it is the answer to one question ("what may an
7
+ * account select here"), and a facade that assembled it itself is a facade that can offer
8
+ * `custom` with nothing behind it, or register stores nothing offers.
9
+ */
10
+ export function withRegisteredBinaryStores(capability, registry) {
11
+ const customStores = registry?.views() ?? [];
12
+ return {
13
+ ...capability,
14
+ supportedBackends: customStores.length
15
+ ? [...capability.supportedBackends, 'custom']
16
+ : capability.supportedBackends,
17
+ customStores,
18
+ };
19
+ }
2
20
  /**
3
21
  * A stable, non-reversible fingerprint of the decrypted S3 credentials, folded into the
4
22
  * cache signature so ROTATING the keys (their presence is unchanged, but the values differ)
@@ -17,6 +35,71 @@ function credentialFingerprint(creds) {
17
35
  }
18
36
  return (hash >>> 0).toString(16);
19
37
  }
38
+ /**
39
+ * Build the blob backend for an account that selected a DEPLOYMENT-REGISTERED store.
40
+ *
41
+ * A failure here does NOT share the built-in backends' silence: an unregistered id is a
42
+ * deployment whose build no longer carries a store its accounts still point at, and nothing else
43
+ * in the system would ever say so. The account's settings page shows exactly what was saved, and
44
+ * the artifacts simply stop being retained. On a MOTHERSHIP deployment this line is the whole
45
+ * signal that the retention sweep is reclaiming nothing, since the process that sweeps is not the
46
+ * process that wrote the bytes.
47
+ *
48
+ * It is said ONCE per account per config. The remembered answers get that from the resolver's
49
+ * cache; a store that DECLINED cannot be remembered, so it carries its own guard: {@link announced}
50
+ * holds the `account::store` pairs already reported, and a later success clears the pair so a
51
+ * store that breaks a second time is reported a second time.
52
+ */
53
+ function buildRegisteredStore(deps, accountId, custom, announced) {
54
+ const storeId = custom?.storeId;
55
+ if (!storeId) {
56
+ // Only reachable through a config written outside the settings API, which refuses this shape.
57
+ deps.logger?.warn('content storage: custom backend selected with no store named', { accountId });
58
+ return { blob: null, remember: true };
59
+ }
60
+ const definition = deps.binaryStoreRegistry?.get(storeId);
61
+ if (!definition) {
62
+ deps.logger?.warn('content storage: no binary store registered under the configured id', {
63
+ accountId,
64
+ storeId,
65
+ registered: deps.binaryStoreRegistry?.ids() ?? [],
66
+ });
67
+ return { blob: null, remember: true };
68
+ }
69
+ const declineKey = `${accountId ?? '__default__'}::${storeId}`;
70
+ const blob = definition.create({
71
+ accountId,
72
+ ...(deps.logger ? { logger: deps.logger } : {}),
73
+ });
74
+ if (!blob) {
75
+ // The store's own "not right now". It said so deliberately, so this is a lower-severity line
76
+ // than the two above and still not silent.
77
+ if (!announced.has(declineKey)) {
78
+ announced.add(declineKey);
79
+ deps.logger?.info('content storage: registered binary store declined to build', {
80
+ accountId,
81
+ storeId,
82
+ });
83
+ }
84
+ return { blob: null, remember: false };
85
+ }
86
+ announced.delete(declineKey);
87
+ // The persisted `storage` value is the REGISTERED id, not whatever the implementation declares.
88
+ // The column's only job is to say which store to ask for these bytes, and the registry is the
89
+ // only party that knows the answer: an implementation reused under two registrations (one bucket
90
+ // per region, say) declares one `kind` for both, and one stamping `s3` would make its rows read
91
+ // as the platform's own S3 backend's. Wrapped rather than spread so a class instance keeps its
92
+ // `this`.
93
+ return {
94
+ blob: {
95
+ kind: definition.id,
96
+ put: (key, bytes, contentType) => blob.put(key, bytes, contentType),
97
+ get: (key) => blob.get(key),
98
+ delete: (key) => blob.delete(key),
99
+ },
100
+ remember: true,
101
+ };
102
+ }
20
103
  /**
21
104
  * Compose a {@link ResolveBinaryArtifactStore}: workspace → owning account → that account's
22
105
  * configured backend (or the runtime default when unconfigured) → a composed
@@ -28,12 +111,19 @@ function credentialFingerprint(creds) {
28
111
  * requests until the config actually changes.
29
112
  */
30
113
  export function makeResolveBinaryArtifactStore(deps) {
114
+ // `store: null` is a CACHED REFUSAL, not an empty slot: an account whose configured backend
115
+ // this build cannot serve gets the same answer every time until its config changes, so
116
+ // re-deriving it per resolve only repeats the work and the log line that named the problem.
117
+ // Only a `settled` refusal is remembered (see {@link BlobResolution}).
31
118
  const cache = new Map();
119
+ // `account::store` pairs whose store has already reported that it cannot build right now.
120
+ const announcedDeclines = new Set();
32
121
  return async (workspaceId) => {
33
122
  const accountId = (await deps.accountOf(workspaceId)) ?? null;
34
123
  let backend = deps.defaultBackend;
35
124
  let fs;
36
125
  let s3;
126
+ let custom;
37
127
  let s3Credentials;
38
128
  if (accountId && deps.accountSettings) {
39
129
  const resolved = await deps.accountSettings.resolve(accountId);
@@ -42,6 +132,7 @@ export function makeResolveBinaryArtifactStore(deps) {
42
132
  backend = cs.backend;
43
133
  fs = cs.fs;
44
134
  s3 = cs.s3;
135
+ custom = cs.custom;
45
136
  s3Credentials = resolved.s3Credentials;
46
137
  }
47
138
  }
@@ -52,17 +143,29 @@ export function makeResolveBinaryArtifactStore(deps) {
52
143
  backend,
53
144
  fs,
54
145
  s3,
146
+ // The store id joins the signature for the same reason the S3 fingerprint does: switching
147
+ // an account between two registered stores changes nothing else here, and a cached store
148
+ // would keep writing into the one it was pointed at when it was built.
149
+ custom,
55
150
  creds: credentialFingerprint(s3Credentials),
56
151
  });
57
152
  const cached = cache.get(cacheKey);
58
153
  if (cached && cached.signature === signature)
59
154
  return cached.store;
60
- const blob = deps.buildBlobBackend(backend, { fs, s3, s3Credentials });
61
- if (!blob)
155
+ const resolution = backend === 'custom'
156
+ ? buildRegisteredStore(deps, accountId, custom, announcedDeclines)
157
+ : // A runtime's own factory is a pure function of the kind plus this account's connection
158
+ // settings, both of which are in the signature, so its `null` ("`fs` on Cloudflare",
159
+ // "`s3` with no connection") cannot change without the signature changing with it.
160
+ { blob: deps.buildBlobBackend(backend, { fs, s3, s3Credentials }), remember: true };
161
+ if (!resolution.blob) {
162
+ if (resolution.remember)
163
+ cache.set(cacheKey, { signature, store: null });
62
164
  return null;
165
+ }
63
166
  const store = createBinaryArtifactStore({
64
167
  metadata: deps.metadata,
65
- blob,
168
+ blob: resolution.blob,
66
169
  idGenerator: deps.idGenerator,
67
170
  clock: deps.clock,
68
171
  ...(deps.logger ? { logger: deps.logger } : {}),
@@ -1 +1 @@
1
- {"version":3,"file":"binaryArtifactStore.js","sourceRoot":"","sources":["../../src/persistence/binaryArtifactStore.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"binaryArtifactStore.js","sourceRoot":"","sources":["../../src/persistence/binaryArtifactStore.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"rpc-allowlist.d.ts","sourceRoot":"","sources":["../../src/persistence/rpc-allowlist.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAgBtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,eAAO,MAAM,0BAA0B,EAAE,sBA0wCxC,CAAA;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,oCAAoC,YAC/C,yBAAyB,EACzB,gCAAgC,EAChC,4BAA4B,EAC5B,yBAAyB,EACzB,2BAA2B,EAC3B,kCAAkC,CAC1B,CAAA;AAEV,yEAAyE;AACzE,MAAM,MAAM,+BAA+B,GAAG,CAAC,OAAO,oCAAoC,CAAC,CAAC,MAAM,CAAC,CAAA"}
1
+ {"version":3,"file":"rpc-allowlist.d.ts","sourceRoot":"","sources":["../../src/persistence/rpc-allowlist.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAgBtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,eAAO,MAAM,0BAA0B,EAAE,sBAkxCxC,CAAA;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,oCAAoC,YAC/C,yBAAyB,EACzB,gCAAgC,EAChC,4BAA4B,EAC5B,yBAAyB,EACzB,2BAA2B,EAC3B,kCAAkC,CAC1B,CAAA;AAEV,yEAAyE;AACzE,MAAM,MAAM,+BAA+B,GAAG,CAAC,OAAO,oCAAoC,CAAC,CAAC,MAAM,CAAC,CAAA"}
@@ -1328,6 +1328,14 @@ export const REMOTE_PERSISTENCE_METHODS = {
1328
1328
  getByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
1329
1329
  upsert: { scope: { kind: 'workspaceField', arg: 0 } },
1330
1330
  },
1331
+ // Per-workspace notification routing (which types this board delivers on which channel). No
1332
+ // secrets, and the same shape as `slackSettingsRepository` above: `getByWorkspace` takes the
1333
+ // workspaceId as arg0 (the `workspace` rule), the record-based `upsert(record)` binds on the
1334
+ // record's `workspaceId` FIELD (the `workspaceField` rule).
1335
+ notificationSettingsRepository: {
1336
+ getByWorkspace: { scope: { kind: 'workspace', arg: 0 } },
1337
+ upsert: { scope: { kind: 'workspaceField', arg: 0 } },
1338
+ },
1331
1339
  // Per-account GitHub-user → Slack-member mapping (for @-mentions). No secrets. Both methods take
1332
1340
  // the accountId as arg0 positionally (`upsert(accountId, entries, at)` — a positional accountId,
1333
1341
  // not a record), so the `account` rule binds both.