@teamscala/service-boot 0.23.291 → 0.23.293

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.
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "name": "@teamscala/service-boot",
8
8
  "type": "module",
9
- "version": "0.23.291",
9
+ "version": "0.23.293",
10
10
  "sideEffects": false,
11
11
  "files": [
12
12
  "src"
@@ -17,7 +17,6 @@
17
17
  "@teamscala/agreement-generation": "*",
18
18
  "@teamscala/api-resend": "*",
19
19
  "@teamscala/auth": "*",
20
- "@teamscala/aws": "*",
21
20
  "@teamscala/cache": "*",
22
21
  "@teamscala/channel": "*",
23
22
  "@teamscala/chat-completion": "*",
@@ -33,6 +32,7 @@
33
32
  "@teamscala/generic-registry": "*",
34
33
  "@teamscala/http": "*",
35
34
  "@teamscala/logger": "*",
35
+ "@teamscala/object-storage": "*",
36
36
  "@teamscala/orpc": "*",
37
37
  "@teamscala/os": "*",
38
38
  "@teamscala/realtime-utils": "*",
@@ -393,6 +393,14 @@
393
393
  "types": "./src/fn-routers/task-sync-dispatch.d.ts",
394
394
  "default": "./src/fn-routers/task-sync-dispatch.ts"
395
395
  },
396
+ "./fn-routers/task-sync-projection": {
397
+ "types": "./src/fn-routers/task-sync-projection.d.ts",
398
+ "default": "./src/fn-routers/task-sync-projection.ts"
399
+ },
400
+ "./fn-routers/task-sync-projection.ts": {
401
+ "types": "./src/fn-routers/task-sync-projection.d.ts",
402
+ "default": "./src/fn-routers/task-sync-projection.ts"
403
+ },
396
404
  "./fn-routers/task-sync-router": {
397
405
  "types": "./src/fn-routers/task-sync-router.d.ts",
398
406
  "default": "./src/fn-routers/task-sync-router.ts"
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Co-located tests for handleAssetProxy's early-return paths: a non-asset path
7
+ * 404s, and an asset path with no resolvable S3 client 502s (getClient returns
8
+ * null when creds/config are absent — the documented "no client" contract).
9
+ */
10
+ import { describe, expect, it } from "bun:test";
11
+ import { handleAssetProxy } from "./asset-proxy.ts";
12
+
13
+ describe("handleAssetProxy", () => {
14
+ it("404s for a path outside /api/assets/", async () => {
15
+ const res = await handleAssetProxy(new Request("https://host.example/elsewhere"));
16
+ expect(res.status).toBe(404);
17
+ });
18
+
19
+ it("404s for an empty asset key", async () => {
20
+ const res = await handleAssetProxy(new Request("https://host.example/api/assets/"));
21
+ expect(res.status).toBe(404);
22
+ });
23
+
24
+ it("reaches the S3 client for a valid asset key (no early 404)", async () => {
25
+ // A valid key passes the path + key guards and proceeds to getClient.
26
+ // With no registry/env in the test context getClient cannot resolve and
27
+ // rejects — proving the handler did NOT short-circuit on the path/key.
28
+ await expect(
29
+ handleAssetProxy(new Request("https://host.example/api/assets/logo.png")),
30
+ ).rejects.toThrow();
31
+ });
32
+ });
@@ -9,7 +9,7 @@
9
9
  * Returns 404 for missing keys, 502 for S3 failures. Never redirects.
10
10
  */
11
11
 
12
- import { getClient } from "@teamscala/aws/create/client/get-client";
12
+ import { getClient } from "@teamscala/object-storage/create/client/get-client";
13
13
  import {
14
14
  buildEtag,
15
15
  negotiateFormat,
@@ -6,7 +6,7 @@
6
6
  * Authenticated asset upload — POST /api/transfers/upload. Restores the
7
7
  * handler deleted with @teamscala/server-files (commit ca5643ef); avatar/logo
8
8
  * upload had 404'd on every service since. Writes to S3 via
9
- * @teamscala/aws/create/write-ops and returns the STORED-form URL
9
+ * @teamscala/object-storage/create/write-ops and returns the STORED-form URL
10
10
  * (`/api/files/<key>`) the client persists into avatar_url/logo_url — the
11
11
  * display path rewrites it to the public `/api/assets/<encoded-key>` proxy
12
12
  * via `toAssetPath` (@teamscala/formatters/asset-url). Self-authenticates
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Co-located test for attachTransfersUpload: it registers the POST
7
+ * /api/transfers/upload route on the host app. Hono is a type-only import
8
+ * (the real app is built by the runtime), so the host interface is mocked
9
+ * here to exercise registration without the Hono dependency.
10
+ */
11
+ import { describe, expect, it } from "bun:test";
12
+ import { attachTransfersUpload } from "./attach-transfers-upload.ts";
13
+
14
+ describe("attachTransfersUpload", () => {
15
+ it("registers POST /api/transfers/upload on the host app", async () => {
16
+ const registered: string[] = [];
17
+ const mockApp = {
18
+ post(path: string, _handler: unknown) {
19
+ registered.push(path);
20
+ },
21
+ };
22
+ await attachTransfersUpload(mockApp as never);
23
+ expect(registered).toContain("/api/transfers/upload");
24
+ });
25
+ });
@@ -6,7 +6,7 @@
6
6
  * Authenticated asset upload — POST /api/transfers/upload. Restores the
7
7
  * handler deleted with @teamscala/server-files (commit ca5643ef); avatar/logo
8
8
  * upload had 404'd on every service since. Writes to S3 via
9
- * @teamscala/aws/create/write-ops and returns the STORED-form URL
9
+ * @teamscala/object-storage/create/write-ops and returns the STORED-form URL
10
10
  * (`/api/files/<key>`) the client persists into avatar_url/logo_url — the
11
11
  * display path rewrites it to the public `/api/assets/<encoded-key>` proxy
12
12
  * via `toAssetPath` (@teamscala/formatters/asset-url). Self-authenticates
@@ -25,7 +25,7 @@
25
25
  */
26
26
  import type { Hono } from "hono";
27
27
  import { getAppLogger } from "@teamscala/logger/app-loggers";
28
- import { upload } from "@teamscala/aws/create/write-ops";
28
+ import { upload } from "@teamscala/object-storage/create/write-ops";
29
29
  import { getSession } from "@teamscala/auth/get-session";
30
30
  import { buildErrorReport } from "@teamscala/os/errors/error-report";
31
31
 
@@ -50,8 +50,8 @@ export declare const cacheAdminRouter: {
50
50
  }>, v.ObjectSchema<{
51
51
  readonly name: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
52
52
  }, undefined>, import("@orpc/contract").Schema<{
53
- flushed: any;
53
+ flushed: number;
54
54
  }, {
55
- flushed: any;
55
+ flushed: number;
56
56
  }>, Record<never, never>, Record<never, never>>;
57
57
  };
@@ -31,7 +31,7 @@ export declare const proposalSendRouter: {
31
31
  readonly baseUrl: v.StringSchema<undefined>;
32
32
  }, undefined>, import("@orpc/contract").Schema<{
33
33
  ok: false;
34
- error: any;
34
+ error: string;
35
35
  sentAt?: undefined;
36
36
  url?: undefined;
37
37
  email?: undefined;
@@ -43,7 +43,7 @@ export declare const proposalSendRouter: {
43
43
  email: string;
44
44
  }, {
45
45
  ok: false;
46
- error: any;
46
+ error: string;
47
47
  sentAt?: undefined;
48
48
  url?: undefined;
49
49
  email?: undefined;
@@ -114,12 +114,12 @@ export declare const publicAgreementRouter: {
114
114
  matched: boolean;
115
115
  multiParty: boolean;
116
116
  name: any;
117
- role: any;
118
- signerName: any;
119
- partySignedAt: any;
120
- allSigned: any;
121
- parties: any;
122
- accessToken: any;
117
+ role: string;
118
+ signerName: string;
119
+ partySignedAt: string | null;
120
+ allSigned: boolean;
121
+ parties: import("@teamscala/document-signing/agreement-parties").PublicAgreementParty[];
122
+ accessToken: string | null;
123
123
  signedAt?: undefined;
124
124
  } | {
125
125
  multiParty?: undefined;
@@ -131,7 +131,7 @@ export declare const publicAgreementRouter: {
131
131
  name: any;
132
132
  signerName: any;
133
133
  signedAt: any;
134
- accessToken: any;
134
+ accessToken: string | null;
135
135
  }, {
136
136
  matched: false;
137
137
  multiParty?: undefined;
@@ -147,12 +147,12 @@ export declare const publicAgreementRouter: {
147
147
  matched: boolean;
148
148
  multiParty: boolean;
149
149
  name: any;
150
- role: any;
151
- signerName: any;
152
- partySignedAt: any;
153
- allSigned: any;
154
- parties: any;
155
- accessToken: any;
150
+ role: string;
151
+ signerName: string;
152
+ partySignedAt: string | null;
153
+ allSigned: boolean;
154
+ parties: import("@teamscala/document-signing/agreement-parties").PublicAgreementParty[];
155
+ accessToken: string | null;
156
156
  signedAt?: undefined;
157
157
  } | {
158
158
  multiParty?: undefined;
@@ -164,7 +164,7 @@ export declare const publicAgreementRouter: {
164
164
  name: any;
165
165
  signerName: any;
166
166
  signedAt: any;
167
- accessToken: any;
167
+ accessToken: string | null;
168
168
  }>, Record<never, never>, Record<never, never>>;
169
169
  sign: import("@orpc/server").DecoratedProcedure<Context & Record<never, never>, Context, v.ObjectSchema<{
170
170
  readonly agreementId: v.StringSchema<undefined>;
@@ -182,10 +182,10 @@ export declare const publicAgreementRouter: {
182
182
  payUrl?: undefined;
183
183
  } | {
184
184
  ok: true;
185
- role: any;
186
- signedAt: any;
187
- allSigned: any;
188
- parties: any;
185
+ role: string;
186
+ signedAt: string;
187
+ allSigned: boolean;
188
+ parties: import("@teamscala/document-signing/agreement-parties").PublicAgreementParty[];
189
189
  payUrl: string | null;
190
190
  } | {
191
191
  error?: undefined;
@@ -210,10 +210,10 @@ export declare const publicAgreementRouter: {
210
210
  payUrl?: undefined;
211
211
  } | {
212
212
  ok: true;
213
- role: any;
214
- signedAt: any;
215
- allSigned: any;
216
- parties: any;
213
+ role: string;
214
+ signedAt: string;
215
+ allSigned: boolean;
216
+ parties: import("@teamscala/document-signing/agreement-parties").PublicAgreementParty[];
217
217
  payUrl: string | null;
218
218
  } | {
219
219
  error?: undefined;
@@ -248,14 +248,18 @@ export declare const publicAgreementRouter: {
248
248
  valid?: undefined;
249
249
  } | {
250
250
  ok: true;
251
- envelopeId: any;
252
- documentName: any;
253
- complete: any;
254
- signedAt: any;
251
+ envelopeId: string;
252
+ documentName: string;
253
+ complete: boolean;
254
+ signedAt: string | null;
255
255
  sealedHash: string | null;
256
256
  recomputedHash: string | null;
257
257
  valid: boolean;
258
- parties: any;
258
+ parties: {
259
+ role: string;
260
+ name: string;
261
+ signedAt: string | null;
262
+ }[];
259
263
  }, {
260
264
  parties?: undefined;
261
265
  signedAt?: undefined;
@@ -268,14 +272,18 @@ export declare const publicAgreementRouter: {
268
272
  valid?: undefined;
269
273
  } | {
270
274
  ok: true;
271
- envelopeId: any;
272
- documentName: any;
273
- complete: any;
274
- signedAt: any;
275
+ envelopeId: string;
276
+ documentName: string;
277
+ complete: boolean;
278
+ signedAt: string | null;
275
279
  sealedHash: string | null;
276
280
  recomputedHash: string | null;
277
281
  valid: boolean;
278
- parties: any;
282
+ parties: {
283
+ role: string;
284
+ name: string;
285
+ signedAt: string | null;
286
+ }[];
279
287
  }>, Record<never, never>, Record<never, never>>;
280
288
  };
281
289
  export {};
@@ -21,7 +21,7 @@ export declare const publicGptSessionRouter: {
21
21
  readonly orgId: v.StringSchema<undefined>;
22
22
  readonly sourceUrl: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
23
23
  }, undefined>, import("@orpc/contract").Schema<{
24
- conversationId: any;
24
+ conversationId: string;
25
25
  contactName: any;
26
26
  contactPhone: any;
27
27
  messages: any;
@@ -29,15 +29,33 @@ export declare const publicGptSessionRouter: {
29
29
  inputTokens: number;
30
30
  outputTokens: number;
31
31
  };
32
- }, {
32
+ } | {
33
33
  conversationId: any;
34
34
  contactName: any;
35
35
  contactPhone: any;
36
+ messages: never[];
37
+ tokenTotals: {
38
+ inputTokens: number;
39
+ outputTokens: number;
40
+ };
41
+ }, {
42
+ conversationId: string;
43
+ contactName: any;
44
+ contactPhone: any;
36
45
  messages: any;
37
46
  tokenTotals: {
38
47
  inputTokens: number;
39
48
  outputTokens: number;
40
49
  };
50
+ } | {
51
+ conversationId: any;
52
+ contactName: any;
53
+ contactPhone: any;
54
+ messages: never[];
55
+ tokenTotals: {
56
+ inputTokens: number;
57
+ outputTokens: number;
58
+ };
41
59
  }>, Record<never, never>, Record<never, never>>;
42
60
  load: import("@orpc/server").DecoratedProcedure<Context & Record<never, never>, Context, v.ObjectSchema<{
43
61
  readonly conversationId: v.StringSchema<undefined>;
@@ -11,7 +11,7 @@ import { os } from "@orpc/server";
11
11
  import * as v from "valibot";
12
12
  import { getAppLogger } from "@teamscala/logger/app-loggers";
13
13
  import { queryRows } from "@teamscala/db/query-rows";
14
- import { getClient } from "@teamscala/aws/create/client/get-client";
14
+ import { getClient } from "@teamscala/object-storage/create/client/get-client";
15
15
  import { persistChatMessages } from "./persist-chat-messages.ts";
16
16
 
17
17
  interface Context {
@@ -21,28 +21,60 @@
21
21
  */
22
22
  import type { Context } from "./auth-router.ts";
23
23
  export declare const signInFlowRouter: {
24
- getSignInFlowConfig: import("@orpc/server").DecoratedProcedure<Context & Record<never, never>, Context, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<any, any>, Record<never, never>, Record<never, never>>;
24
+ getSignInFlowConfig: import("@orpc/server").DecoratedProcedure<Context & Record<never, never>, Context, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<{
25
+ signInPath: string;
26
+ portalPathPrefix: string;
27
+ redirectQueryParam: string;
28
+ sessionCheckEndpoint: string;
29
+ portalsEndpoint: string;
30
+ clearStaleCookiesEndpoint: string;
31
+ portalRedirects: {
32
+ orgDashboardPathTemplate: string;
33
+ individualDashboardPathTemplate: string;
34
+ };
35
+ }, {
36
+ signInPath: string;
37
+ portalPathPrefix: string;
38
+ redirectQueryParam: string;
39
+ sessionCheckEndpoint: string;
40
+ portalsEndpoint: string;
41
+ clearStaleCookiesEndpoint: string;
42
+ portalRedirects: {
43
+ orgDashboardPathTemplate: string;
44
+ individualDashboardPathTemplate: string;
45
+ };
46
+ }>, Record<never, never>, Record<never, never>>;
25
47
  checkSession: import("@orpc/server").DecoratedProcedure<Context & Record<never, never>, Context, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<{
26
48
  user?: undefined;
27
49
  redirectUrl?: undefined;
28
50
  portalCount?: undefined;
29
51
  } | {
30
- user: any;
31
- redirectUrl: any;
52
+ user: {
53
+ id: string;
54
+ email: string;
55
+ name: string | null;
56
+ role: string | null;
57
+ };
58
+ redirectUrl: string;
32
59
  portalCount: number;
33
60
  }, {
34
61
  user?: undefined;
35
62
  redirectUrl?: undefined;
36
63
  portalCount?: undefined;
37
64
  } | {
38
- user: any;
39
- redirectUrl: any;
65
+ user: {
66
+ id: string;
67
+ email: string;
68
+ name: string | null;
69
+ role: string | null;
70
+ };
71
+ redirectUrl: string;
40
72
  portalCount: number;
41
73
  }>, Record<never, never>, Record<never, never>>;
42
74
  listPortals: import("@orpc/server").DecoratedProcedure<Context & Record<never, never>, Context, import("@orpc/contract").Schema<unknown, unknown>, import("@orpc/contract").Schema<{
43
75
  portals: never[];
44
76
  userOrgId: null;
45
- userName: any;
77
+ userName: string | null;
46
78
  } | {
47
79
  portals: {
48
80
  id: unknown;
@@ -54,11 +86,11 @@ export declare const signInFlowRouter: {
54
86
  portal_type: {} | null;
55
87
  }[];
56
88
  userOrgId: string;
57
- userName: any;
89
+ userName: string | null;
58
90
  }, {
59
91
  portals: never[];
60
92
  userOrgId: null;
61
- userName: any;
93
+ userName: string | null;
62
94
  } | {
63
95
  portals: {
64
96
  id: unknown;
@@ -70,6 +102,6 @@ export declare const signInFlowRouter: {
70
102
  portal_type: {} | null;
71
103
  }[];
72
104
  userOrgId: string;
73
- userName: any;
105
+ userName: string | null;
74
106
  }>, Record<never, never>, Record<never, never>>;
75
107
  };
@@ -0,0 +1,120 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * The task-sync PROJECTION: how a CLI's task list maps onto `work_items` rows,
7
+ * and what reconciling one against the other should do.
8
+ *
9
+ * Split out of `task-sync-router.ts`, which had grown past the 1000-line budget
10
+ * and so could not be committed by anyone touching it. The seam is real rather
11
+ * than arbitrary: everything here is PURE and decides WHAT should change, while
12
+ * the router owns the ORPC surface and performs the writes. That is exactly why
13
+ * this half is directly testable with no database.
14
+ *
15
+ * CAPABILITY HARVEST (capability-harvest-on-split): APP-LOCAL, not a capability
16
+ * candidate. Checked against the 14 platform singletons and the package catalog
17
+ * — nothing generic to fold into, and the doctrine test fails: no unrelated app
18
+ * could install this unmodified, because every export is bound to the
19
+ * `work_items` schema and task-sync's own contract (STATUS_TO_WORK_ITEM encodes
20
+ * the work_items status enum, subtaskKey is this projection's key convention,
21
+ * planReconcile diffs against work_items subtask rows).
22
+ *
23
+ * The near-miss is worth naming so the next reader does not re-derive it: the
24
+ * SHAPE — diff a desired list against existing rows into create/update/delete —
25
+ * is generic, and `document-signing` is precedent for lifting a pure domain half
26
+ * out of service-boot into a package. It stays here because it has exactly one
27
+ * consumer; extracting it now would be speculative packaging, which
28
+ * platform-capabilities treats as drift. Revisit if a SECOND surface needs
29
+ * list-against-rows reconciliation — then the generic diff core (not the
30
+ * work_items-specific mapping) is what earns a package.
31
+ */
32
+ import type { TaskListItem, WorkItem } from "./task-sync-router.ts";
33
+ export declare const STATUS_TO_WORK_ITEM: Record<TaskListItem["status"], string>;
34
+ export declare function normalizeStatus(s: string | null): TaskListItem["status"];
35
+ /**
36
+ * Decide the status to WRITE for an existing row, given what the CLI list says.
37
+ *
38
+ * The CLI task list has no `blocked` state, so a blocked row round-trips as
39
+ * `pending` (via `normalizeStatus`, which collapses unknown/internal statuses)
40
+ * and then maps back to `todo` — silently destroying the block. That is not a
41
+ * cosmetic drift: the stop gate reads these rows, so un-blocking one re-blocks
42
+ * the LANE from stopping, and the lane's only apparent fix is to touch the list
43
+ * again, which destroys it again.
44
+ *
45
+ * A `todo` arriving from the CLI therefore carries NO information: it is what
46
+ * the projection emits for every status it cannot represent, not an assertion
47
+ * that the work became un-blocked. `in-progress` and `done` are different —
48
+ * those are real transitions the CLI CAN express, so they win normally.
49
+ *
50
+ * Measured 2026-07-30: a lane set a row to `blocked` with a named blocker, the
51
+ * next TaskUpdate reset it to `todo`, and the stop gate reported the lane
52
+ * unfinished again — three turns of ping-pong before the cause was found.
53
+ */
54
+ export declare function resolveProjectedStatus(currentStatus: string | null | undefined, incomingStatus: string): string;
55
+ export declare function subtaskKey(item: {
56
+ mark?: string;
57
+ content: string;
58
+ title: string;
59
+ }): string;
60
+ /**
61
+ * Pure decision for a TaskUpdate/TaskDelete: which existing subtask does this
62
+ * event address, and what should happen when none does. Extracted for the same
63
+ * reason planReconcile was — the handler should only EXECUTE, so the matching
64
+ * rule is unit-testable.
65
+ *
66
+ * Three resolutions:
67
+ * - `update` — matched by mark, or (mark absent/unstamped) by exact title.
68
+ * - `heal` — nothing matched but the event carries a subject, so the row can
69
+ * be reconstructed. Covers a TaskCreate that silently produced no
70
+ * subtask, which previously orphaned the task forever.
71
+ * - `discard` — nothing matched and there is no subject to rebuild a title from.
72
+ *
73
+ * Measured 2026-07-28: marks "1" and "3" were stamped for one lane while mark
74
+ * "2" was never written at all, and every later update for it was dropped as
75
+ * `no-matching-subtask` — 26 of 30 tasksync discards in six hours, across seven
76
+ * distinct lane sessions, were that one reason.
77
+ */
78
+ export declare function resolveTaskUpdateTarget(existing: Array<{
79
+ id: string;
80
+ title: string;
81
+ kind_data?: Record<string, unknown> | null;
82
+ }>, evt: {
83
+ taskId: string;
84
+ subject?: string;
85
+ }): {
86
+ op: "update";
87
+ id: string;
88
+ } | {
89
+ op: "heal";
90
+ title: string;
91
+ } | {
92
+ op: "discard";
93
+ };
94
+ /**
95
+ * Pure reconciliation plan: given the lane's EXISTING subtasks and its INCOMING
96
+ * (current) task list, compute the create/update/delete ops that make the
97
+ * subtask set match the list — WITHOUT touching prisma. Extracted so the
98
+ * matching/status/positioning logic is unit-testable (the handler only executes
99
+ * the plan). Matching is by stable mark when the CLI provides one, else by
100
+ * content. Position follows incoming index, so a reorder is an update.
101
+ */
102
+ export interface ReconcilePlan {
103
+ toCreate: Array<{
104
+ content: string;
105
+ status: string;
106
+ completed: boolean;
107
+ position: string;
108
+ kind_data: Record<string, unknown>;
109
+ }>;
110
+ toUpdate: Array<{
111
+ id: string;
112
+ content: string;
113
+ status: string;
114
+ completed: boolean;
115
+ position: string;
116
+ kind_data: Record<string, unknown>;
117
+ }>;
118
+ toDelete: string[];
119
+ }
120
+ export declare function planReconcile(existing: WorkItem[], incoming: TaskListItem[], source?: string): ReconcilePlan;