@flowajs/chat-service 0.0.1

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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -0
  3. package/dist/artifact.d.ts +87 -0
  4. package/dist/artifact.d.ts.map +1 -0
  5. package/dist/artifact.js +99 -0
  6. package/dist/artifact.js.map +1 -0
  7. package/dist/audit.d.ts +28 -0
  8. package/dist/audit.d.ts.map +1 -0
  9. package/dist/audit.js +73 -0
  10. package/dist/audit.js.map +1 -0
  11. package/dist/auth/jwt.d.ts +18 -0
  12. package/dist/auth/jwt.d.ts.map +1 -0
  13. package/dist/auth/jwt.js +23 -0
  14. package/dist/auth/jwt.js.map +1 -0
  15. package/dist/auth/oidc.d.ts +30 -0
  16. package/dist/auth/oidc.d.ts.map +1 -0
  17. package/dist/auth/oidc.js +58 -0
  18. package/dist/auth/oidc.js.map +1 -0
  19. package/dist/chat.d.ts +161 -0
  20. package/dist/chat.d.ts.map +1 -0
  21. package/dist/chat.js +636 -0
  22. package/dist/chat.js.map +1 -0
  23. package/dist/cli.d.ts +6 -0
  24. package/dist/cli.d.ts.map +1 -0
  25. package/dist/cli.js +47 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/config.d.ts +18 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +80 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/index.d.ts +19 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +19 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/instrumentation.d.ts +31 -0
  36. package/dist/instrumentation.d.ts.map +1 -0
  37. package/dist/instrumentation.js +151 -0
  38. package/dist/instrumentation.js.map +1 -0
  39. package/dist/llm/anthropic.d.ts +22 -0
  40. package/dist/llm/anthropic.d.ts.map +1 -0
  41. package/dist/llm/anthropic.js +40 -0
  42. package/dist/llm/anthropic.js.map +1 -0
  43. package/dist/llm/bedrock.d.ts +34 -0
  44. package/dist/llm/bedrock.d.ts.map +1 -0
  45. package/dist/llm/bedrock.js +54 -0
  46. package/dist/llm/bedrock.js.map +1 -0
  47. package/dist/llm/factory.d.ts +3 -0
  48. package/dist/llm/factory.d.ts.map +1 -0
  49. package/dist/llm/factory.js +59 -0
  50. package/dist/llm/factory.js.map +1 -0
  51. package/dist/llm/google-gla.d.ts +22 -0
  52. package/dist/llm/google-gla.d.ts.map +1 -0
  53. package/dist/llm/google-gla.js +29 -0
  54. package/dist/llm/google-gla.js.map +1 -0
  55. package/dist/llm/google-vertex.d.ts +21 -0
  56. package/dist/llm/google-vertex.d.ts.map +1 -0
  57. package/dist/llm/google-vertex.js +28 -0
  58. package/dist/llm/google-vertex.js.map +1 -0
  59. package/dist/llm/interface.d.ts +45 -0
  60. package/dist/llm/interface.d.ts.map +1 -0
  61. package/dist/llm/interface.js +2 -0
  62. package/dist/llm/interface.js.map +1 -0
  63. package/dist/llm/openai.d.ts +19 -0
  64. package/dist/llm/openai.d.ts.map +1 -0
  65. package/dist/llm/openai.js +25 -0
  66. package/dist/llm/openai.js.map +1 -0
  67. package/dist/prompts.d.ts +7 -0
  68. package/dist/prompts.d.ts.map +1 -0
  69. package/dist/prompts.js +17 -0
  70. package/dist/prompts.js.map +1 -0
  71. package/dist/server.d.ts +39 -0
  72. package/dist/server.d.ts.map +1 -0
  73. package/dist/server.js +106 -0
  74. package/dist/server.js.map +1 -0
  75. package/dist/session.d.ts +68 -0
  76. package/dist/session.d.ts.map +1 -0
  77. package/dist/session.js +245 -0
  78. package/dist/session.js.map +1 -0
  79. package/dist/storage/factory.d.ts +28 -0
  80. package/dist/storage/factory.d.ts.map +1 -0
  81. package/dist/storage/factory.js +33 -0
  82. package/dist/storage/factory.js.map +1 -0
  83. package/dist/storage/fs.d.ts +14 -0
  84. package/dist/storage/fs.d.ts.map +1 -0
  85. package/dist/storage/fs.js +116 -0
  86. package/dist/storage/fs.js.map +1 -0
  87. package/dist/storage/gcs.d.ts +27 -0
  88. package/dist/storage/gcs.d.ts.map +1 -0
  89. package/dist/storage/gcs.js +81 -0
  90. package/dist/storage/gcs.js.map +1 -0
  91. package/dist/storage/interface.d.ts +33 -0
  92. package/dist/storage/interface.d.ts.map +1 -0
  93. package/dist/storage/interface.js +12 -0
  94. package/dist/storage/interface.js.map +1 -0
  95. package/dist/storage/s3.d.ts +29 -0
  96. package/dist/storage/s3.d.ts.map +1 -0
  97. package/dist/storage/s3.js +109 -0
  98. package/dist/storage/s3.js.map +1 -0
  99. package/dist/storage-keys.d.ts +33 -0
  100. package/dist/storage-keys.d.ts.map +1 -0
  101. package/dist/storage-keys.js +76 -0
  102. package/dist/storage-keys.js.map +1 -0
  103. package/dist/telemetry.d.ts +29 -0
  104. package/dist/telemetry.d.ts.map +1 -0
  105. package/dist/telemetry.js +116 -0
  106. package/dist/telemetry.js.map +1 -0
  107. package/dist/yaml.d.ts +42 -0
  108. package/dist/yaml.d.ts.map +1 -0
  109. package/dist/yaml.js +121 -0
  110. package/dist/yaml.js.map +1 -0
  111. package/package.json +124 -0
@@ -0,0 +1,245 @@
1
+ /** Session management: creation, caching, and context building. */
2
+ import { randomUUID } from "node:crypto";
3
+ import nunjucks from "nunjucks";
4
+ import { signSessionToken } from "./auth/jwt.js";
5
+ import { loadQueryResult, loadAggregate, loadPaperMetadata, listEditDrafts, } from "./storage-keys.js";
6
+ import { schemaForPrompt } from "./artifact.js";
7
+ import { loadEditPromptTemplate } from "./prompts.js";
8
+ import { buildBboxCache, artifactToYaml, addLineNumbers, } from "./yaml.js";
9
+ /** In-memory session cache. Not the source of truth — rebuilt from storage on miss. */
10
+ const sessions = new Map();
11
+ let sweeper = null;
12
+ function ensureSweeper() {
13
+ if (sweeper)
14
+ return;
15
+ sweeper = setInterval(() => {
16
+ const now = Date.now();
17
+ for (const [id, ctx] of sessions) {
18
+ if (ctx.expiresAt.getTime() < now)
19
+ sessions.delete(id);
20
+ }
21
+ }, 30 * 60 * 1000);
22
+ sweeper.unref();
23
+ }
24
+ export function getCachedSession(sessionId) {
25
+ return sessions.get(sessionId);
26
+ }
27
+ /** For tests / shutdown: clear the in-memory cache. */
28
+ export function clearSessionCache() {
29
+ sessions.clear();
30
+ if (sweeper) {
31
+ clearInterval(sweeper);
32
+ sweeper = null;
33
+ }
34
+ }
35
+ /**
36
+ * Rebuild session context from JWT claims (after restart or cache eviction).
37
+ * Loads the latest draft from storage to restore artifact state. This loses
38
+ * session-specific position if another session has written newer versions
39
+ * in the meantime — acceptable for now.
40
+ */
41
+ export async function rebuildSession(config, claims, expiresAt) {
42
+ ensureSweeper();
43
+ const category = claims.category;
44
+ const query = await loadQueryResult(config.storage, claims.variant_id);
45
+ if (!query) {
46
+ throw new Error(`No assessment data found for variant ${claims.variant_id}`);
47
+ }
48
+ const [aggregate, drafts, ...paperMetadataResults] = await Promise.all([
49
+ loadAggregate(config.storage, claims.variant_id),
50
+ listEditDrafts(config.storage, claims.variant_id, category),
51
+ ...query.dois.map((doi) => loadPaperMetadata(config.storage, doi)),
52
+ ]);
53
+ const paperMetadata = query.dois.map((doi, i) => ({
54
+ doi,
55
+ metadata: paperMetadataResults[i] ?? null,
56
+ }));
57
+ const paperIds = buildPaperIds(aggregate);
58
+ const latestDraft = drafts.length > 0 ? drafts[drafts.length - 1] : null;
59
+ let artifactJson;
60
+ let artifactVersion;
61
+ if (latestDraft) {
62
+ artifactJson = latestDraft.artifactText;
63
+ artifactVersion = latestDraft.version;
64
+ }
65
+ else {
66
+ artifactJson = extractArtifactFromAggregate(aggregate, category);
67
+ artifactVersion = 0;
68
+ }
69
+ const bboxCache = buildBboxCache(artifactJson);
70
+ const artifactYaml = artifactToYaml(artifactJson);
71
+ const session = {
72
+ id: claims.session_id,
73
+ variantId: claims.variant_id,
74
+ userId: claims.user_id,
75
+ paperIds,
76
+ systemPrompt: buildEditSystemPrompt({
77
+ papers: paperMetadata,
78
+ paperIds,
79
+ artifactYaml,
80
+ schema: config.schema,
81
+ promptDir: config.promptDir,
82
+ }),
83
+ expiresAt,
84
+ artifactYaml,
85
+ artifactVersion,
86
+ artifactDirty: false,
87
+ category,
88
+ aggregateCategories: extractAggregateCategories(aggregate),
89
+ bboxCache,
90
+ };
91
+ sessions.set(session.id, session);
92
+ return session;
93
+ }
94
+ // ---------------------------------------------------------------------------
95
+ // Edit sessions
96
+ // ---------------------------------------------------------------------------
97
+ const promptEnv = new nunjucks.Environment(undefined, {
98
+ autoescape: false,
99
+ throwOnUndefined: true,
100
+ });
101
+ /**
102
+ * Create a new edit session bound to a specific artifact version. The caller
103
+ * passes the initial artifact JSON verbatim and the version number it came
104
+ * from — chat-service stores these as-is.
105
+ */
106
+ export async function createEditSession(config, input) {
107
+ ensureSweeper();
108
+ const query = await loadQueryResult(config.storage, input.variantId);
109
+ if (!query) {
110
+ throw new Error(`No assessment data found for variant ${input.variantId}`);
111
+ }
112
+ const [aggregate, ...paperMetadataResults] = await Promise.all([
113
+ loadAggregate(config.storage, input.variantId),
114
+ ...query.dois.map((doi) => loadPaperMetadata(config.storage, doi)),
115
+ ]);
116
+ if (!aggregate) {
117
+ throw new Error(`No aggregate found for variant ${input.variantId}`);
118
+ }
119
+ const paperMetadata = query.dois.map((doi, i) => ({
120
+ doi,
121
+ metadata: paperMetadataResults[i] ?? null,
122
+ }));
123
+ const paperIds = buildPaperIds(aggregate);
124
+ const aggregateCategories = extractAggregateCategories(aggregate);
125
+ const bboxCache = buildBboxCache(input.initialArtifact);
126
+ const artifactYaml = artifactToYaml(input.initialArtifact);
127
+ const systemPrompt = buildEditSystemPrompt({
128
+ papers: paperMetadata,
129
+ paperIds,
130
+ artifactYaml,
131
+ schema: config.schema,
132
+ promptDir: config.promptDir,
133
+ });
134
+ const sessionId = randomUUID();
135
+ const claims = {
136
+ session_id: sessionId,
137
+ variant_id: input.variantId,
138
+ user_id: input.userId,
139
+ category: input.category,
140
+ };
141
+ const { token, expiresAt } = await signSessionToken(claims, {
142
+ secret: config.jwtSecret,
143
+ ttlSeconds: config.jwtTtlSeconds,
144
+ });
145
+ const session = {
146
+ id: sessionId,
147
+ variantId: input.variantId,
148
+ userId: input.userId,
149
+ paperIds,
150
+ systemPrompt,
151
+ expiresAt,
152
+ artifactYaml,
153
+ artifactVersion: input.initialVersion,
154
+ artifactDirty: false,
155
+ category: input.category,
156
+ aggregateCategories,
157
+ bboxCache,
158
+ };
159
+ sessions.set(sessionId, session);
160
+ return { session, token, expiresAt };
161
+ }
162
+ /** Extract the artifact JSON for a category from aggregation.json. */
163
+ function extractArtifactFromAggregate(aggregate, category) {
164
+ if (typeof aggregate !== "object" ||
165
+ aggregate === null ||
166
+ !("results" in aggregate)) {
167
+ throw new Error("aggregation.json has no results");
168
+ }
169
+ const results = aggregate.results;
170
+ const result = results.find((r) => r.category === category);
171
+ if (!result) {
172
+ throw new Error(`Category ${category} not found in aggregation.json`);
173
+ }
174
+ return JSON.stringify(result, null, 2);
175
+ }
176
+ /** Extract all categories present in aggregation.json results. */
177
+ function extractAggregateCategories(aggregate) {
178
+ if (typeof aggregate !== "object" ||
179
+ aggregate === null ||
180
+ !("results" in aggregate)) {
181
+ return [];
182
+ }
183
+ return aggregate.results.map((r) => r.category);
184
+ }
185
+ // ---------------------------------------------------------------------------
186
+ // Shared helpers
187
+ // ---------------------------------------------------------------------------
188
+ export const CITATION_INSTRUCTIONS = `## Citations
189
+
190
+ When referencing a finding from a paper, use inline Markdown citation links with a verbatim quote: \`[link text](#cite:paperId "verbatim quote")\`.
191
+
192
+ - The **link text** is free-form — use a descriptive phrase or specific claim, whatever reads naturally.
193
+ - The **title attribute** (in quotes after the href) must be a verbatim passage from the paper text — enough context to validate the claim (typically a sentence or key clause). The quote will be highlighted in the PDF for reviewers, so avoid quoting isolated values or single words that could match multiple locations — include surrounding context.
194
+ - When re-citing a finding from a paper's extraction results (loaded via loadPaperExtracts), reuse the exact quote string from the extraction's citation — do not rephrase or shorten it.
195
+ - When citing from a paper's full text (loaded via loadFullPaper or queryPapers), quote the relevant passage verbatim.
196
+ - The href format \`#cite:paperId\` is required exactly — the application uses it to identify the paper.
197
+ - Be generous with links: whenever a factual claim can be traced to a specific passage, link it.`;
198
+ /** Extract paper ID → DOI mapping from the aggregate's paper_id_mapping. */
199
+ function buildPaperIds(aggregate) {
200
+ const ids = {};
201
+ if (typeof aggregate === "object" &&
202
+ aggregate !== null &&
203
+ "paper_id_mapping" in aggregate) {
204
+ const mapping = aggregate.paper_id_mapping;
205
+ for (const [authorYear, entry] of Object.entries(mapping)) {
206
+ ids[authorYear] = entry.doi;
207
+ }
208
+ }
209
+ return ids;
210
+ }
211
+ function invertPaperIds(paperIds) {
212
+ const inverted = {};
213
+ for (const [id, doi] of Object.entries(paperIds)) {
214
+ inverted[doi] = id;
215
+ }
216
+ return inverted;
217
+ }
218
+ function buildPaperIndex(papers, paperIds) {
219
+ const doiToPaperId = invertPaperIds(paperIds);
220
+ return papers
221
+ .map((p) => {
222
+ const id = doiToPaperId[p.doi];
223
+ if (!id)
224
+ return null;
225
+ const m = p.metadata;
226
+ if (!m)
227
+ return `- **${id}** — metadata unavailable`;
228
+ const title = m.title ?? "Unknown title";
229
+ const abstract = m.abstract ?? "";
230
+ const pmid = m.pmid ? ` (PMID ${m.pmid})` : "";
231
+ return `- **${id}**${pmid}: ${title}\n ${abstract}`;
232
+ })
233
+ .filter(Boolean)
234
+ .join("\n\n");
235
+ }
236
+ function buildEditSystemPrompt({ papers, paperIds, artifactYaml, schema, promptDir, }) {
237
+ const template = loadEditPromptTemplate(promptDir);
238
+ const paperIndex = buildPaperIndex(papers, paperIds);
239
+ return promptEnv.renderString(template, {
240
+ artifact_schema: schemaForPrompt(schema),
241
+ paper_index: paperIndex,
242
+ initial_artifact: addLineNumbers(artifactYaml),
243
+ });
244
+ }
245
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import type { Storage } from "./interface.js";
2
+ /**
3
+ * Discriminated config for the env-driven default `index.ts`. Each backend
4
+ * lives in its own module so the matching SDK (`@aws-sdk/client-s3` for
5
+ * `s3`, `@google-cloud/storage` for `gcs`) is loaded only when actually
6
+ * selected.
7
+ */
8
+ export type StorageConfig = {
9
+ backend: "fs";
10
+ root: string;
11
+ prefix?: string;
12
+ } | {
13
+ backend: "s3";
14
+ bucket: string;
15
+ prefix?: string;
16
+ } | {
17
+ backend: "gcs";
18
+ bucket: string;
19
+ prefix?: string;
20
+ };
21
+ /**
22
+ * Construct a `Storage` from a typed config. The matching backend module is
23
+ * dynamic-imported, so a deployment that only uses `fs` never loads
24
+ * `@aws-sdk/client-s3`. If the peer SDK is missing at runtime, the
25
+ * dynamic import fails with a clear error naming the unresolvable package.
26
+ */
27
+ export declare function createStorage(config: StorageConfig): Promise<Storage>;
28
+ //# sourceMappingURL=factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/storage/factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAE9C;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CA2B3E"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Construct a `Storage` from a typed config. The matching backend module is
3
+ * dynamic-imported, so a deployment that only uses `fs` never loads
4
+ * `@aws-sdk/client-s3`. If the peer SDK is missing at runtime, the
5
+ * dynamic import fails with a clear error naming the unresolvable package.
6
+ */
7
+ export async function createStorage(config) {
8
+ if (config.backend === "fs") {
9
+ const { createFsStorage } = await import("./fs.js");
10
+ return createFsStorage({
11
+ root: config.root,
12
+ ...(config.prefix !== undefined ? { prefix: config.prefix } : {}),
13
+ });
14
+ }
15
+ if (config.backend === "s3") {
16
+ const { createS3Storage } = await import("./s3.js");
17
+ return createS3Storage({
18
+ bucket: config.bucket,
19
+ ...(config.prefix !== undefined ? { prefix: config.prefix } : {}),
20
+ });
21
+ }
22
+ if (config.backend === "gcs") {
23
+ const { createGcsStorage } = await import("./gcs.js");
24
+ return createGcsStorage({
25
+ bucket: config.bucket,
26
+ ...(config.prefix !== undefined ? { prefix: config.prefix } : {}),
27
+ });
28
+ }
29
+ // Exhaustiveness check — the discriminated union should cover every backend.
30
+ const _exhaustive = config;
31
+ throw new Error(`Unsupported storage backend: ${JSON.stringify(_exhaustive)}`);
32
+ }
33
+ //# sourceMappingURL=factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.js","sourceRoot":"","sources":["../../src/storage/factory.ts"],"names":[],"mappings":"AAaA;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAqB;IACvD,IAAI,MAAM,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;QAC5B,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC;QACpD,OAAO,eAAe,CAAC;YACrB,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,GAAG,CAAC,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAClE,CAAC,CAAC;IACL,CAAC;IACD,IAAI,MAAM,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;QAC5B,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC;QACpD,OAAO,eAAe,CAAC;YACrB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,GAAG,CAAC,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAClE,CAAC,CAAC;IACL,CAAC;IACD,IAAI,MAAM,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;QAC7B,MAAM,EAAE,gBAAgB,EAAE,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC;QACtD,OAAO,gBAAgB,CAAC;YACtB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,GAAG,CAAC,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAClE,CAAC,CAAC;IACL,CAAC;IACD,6EAA6E;IAC7E,MAAM,WAAW,GAAU,MAAM,CAAC;IAClC,MAAM,IAAI,KAAK,CACb,gCAAgC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,EAAE,CAC9D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { type Storage } from "./interface.js";
2
+ export interface FsStorageOptions {
3
+ /** Absolute root directory for all keys. Created on first write if missing. */
4
+ root: string;
5
+ /** Optional prefix prepended to every key (joined as a path segment). */
6
+ prefix?: string;
7
+ }
8
+ /**
9
+ * Local filesystem `Storage` backend. Atomic create-only via
10
+ * `O_CREAT|O_EXCL`. Concurrent calls to `writeIfAbsent` for the same key
11
+ * deterministically pick one winner; the loser sees `StorageConflictError`.
12
+ */
13
+ export declare function createFsStorage(options: FsStorageOptions): Storage;
14
+ //# sourceMappingURL=fs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/storage/fs.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,OAAO,EAAwB,MAAM,gBAAgB,CAAC;AAEpE,MAAM,WAAW,gBAAgB;IAC/B,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CA2GlE"}
@@ -0,0 +1,116 @@
1
+ import { open, readFile, writeFile, mkdir, stat, readdir, } from "node:fs/promises";
2
+ import { dirname, join, resolve, sep } from "node:path";
3
+ import { StorageConflictError } from "./interface.js";
4
+ /**
5
+ * Local filesystem `Storage` backend. Atomic create-only via
6
+ * `O_CREAT|O_EXCL`. Concurrent calls to `writeIfAbsent` for the same key
7
+ * deterministically pick one winner; the loser sees `StorageConflictError`.
8
+ */
9
+ export function createFsStorage(options) {
10
+ const root = resolve(options.root);
11
+ const prefix = options.prefix ?? "";
12
+ function pathFor(key) {
13
+ const full = join(root, prefix, key);
14
+ const resolved = resolve(full);
15
+ // Reject path-traversal attempts (key="../.." etc.).
16
+ if (!resolved.startsWith(root + sep) && resolved !== root) {
17
+ throw new Error(`Path traversal rejected for key: ${key}`);
18
+ }
19
+ return resolved;
20
+ }
21
+ return {
22
+ prefix,
23
+ async read(key) {
24
+ try {
25
+ return await readFile(pathFor(key));
26
+ }
27
+ catch (error) {
28
+ if (error.code === "ENOENT")
29
+ return null;
30
+ throw error;
31
+ }
32
+ },
33
+ async readText(key) {
34
+ const buf = await this.read(key);
35
+ return buf ? buf.toString("utf-8") : null;
36
+ },
37
+ async readJson(key) {
38
+ const text = await this.readText(key);
39
+ return text ? JSON.parse(text) : null;
40
+ },
41
+ async write(key, body) {
42
+ const path = pathFor(key);
43
+ await mkdir(dirname(path), { recursive: true });
44
+ await writeFile(path, body);
45
+ },
46
+ async writeJson(key, value) {
47
+ await this.write(key, JSON.stringify(value));
48
+ },
49
+ async writeIfAbsent(key, body) {
50
+ const path = pathFor(key);
51
+ await mkdir(dirname(path), { recursive: true });
52
+ let handle;
53
+ try {
54
+ // O_CREAT|O_EXCL: create only if not exists; EEXIST otherwise.
55
+ handle = await open(path, "wx");
56
+ }
57
+ catch (error) {
58
+ if (error.code === "EEXIST") {
59
+ throw new StorageConflictError(key);
60
+ }
61
+ throw error;
62
+ }
63
+ try {
64
+ await handle.writeFile(body);
65
+ }
66
+ finally {
67
+ await handle.close();
68
+ }
69
+ },
70
+ async exists(key) {
71
+ try {
72
+ await stat(pathFor(key));
73
+ return true;
74
+ }
75
+ catch (error) {
76
+ if (error.code === "ENOENT")
77
+ return false;
78
+ throw error;
79
+ }
80
+ },
81
+ async list(listPrefix) {
82
+ const base = pathFor(listPrefix);
83
+ const baseRoot = pathFor("");
84
+ const out = [];
85
+ async function walk(dir) {
86
+ let entries;
87
+ try {
88
+ entries = await readdir(dir, { withFileTypes: true });
89
+ }
90
+ catch (error) {
91
+ if (error.code === "ENOENT")
92
+ return;
93
+ throw error;
94
+ }
95
+ for (const entry of entries) {
96
+ const full = join(dir, entry.name);
97
+ if (entry.isDirectory()) {
98
+ await walk(full);
99
+ }
100
+ else if (entry.isFile()) {
101
+ // Return keys relative to the configured root (so list output is
102
+ // symmetric with read/write keys).
103
+ const rel = full
104
+ .slice(baseRoot.length)
105
+ .replace(/^[/\\]/, "")
106
+ .replaceAll(sep, "/");
107
+ out.push(rel);
108
+ }
109
+ }
110
+ }
111
+ await walk(base);
112
+ return out.sort();
113
+ },
114
+ };
115
+ }
116
+ //# sourceMappingURL=fs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../../src/storage/fs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { Storage as GcsStorageClient } from "@google-cloud/storage";
2
+ import { type Storage } from "./interface.js";
3
+ /**
4
+ * Env-driven form: chat-service constructs the GCS client with no
5
+ * explicit config; the SDK resolves credentials from Application Default
6
+ * Credentials (`GOOGLE_APPLICATION_CREDENTIALS`, gcloud user creds, GCE
7
+ * metadata server, etc.). For deployments needing custom client config
8
+ * (custom endpoint for emulators, programmatic credential injection,
9
+ * etc.), use the `{ client }` programmatic form below.
10
+ */
11
+ export interface GcsStorageConfigOptions {
12
+ bucket: string;
13
+ prefix?: string;
14
+ }
15
+ /**
16
+ * Programmatic form: caller hands in a pre-built GCS `Storage` client.
17
+ * Use this when the deployment needs custom credential minting (Workload
18
+ * Identity Federation, custom token exchange, etc.) that the default
19
+ * chain does not cover.
20
+ */
21
+ export interface GcsStorageClientOptions {
22
+ client: GcsStorageClient;
23
+ bucket: string;
24
+ prefix?: string;
25
+ }
26
+ export declare function createGcsStorage(options: GcsStorageConfigOptions | GcsStorageClientOptions): Storage;
27
+ //# sourceMappingURL=gcs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gcs.d.ts","sourceRoot":"","sources":["../../src/storage/gcs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,KAAK,OAAO,EAAwB,MAAM,gBAAgB,CAAC;AAEpE;;;;;;;GAOG;AACH,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,gBAAgB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,uBAAuB,GAAG,uBAAuB,GACzD,OAAO,CAaT"}
@@ -0,0 +1,81 @@
1
+ import { Storage as GcsStorageClient } from "@google-cloud/storage";
2
+ import { StorageConflictError } from "./interface.js";
3
+ export function createGcsStorage(options) {
4
+ if ("client" in options) {
5
+ return makeStorage({
6
+ client: options.client,
7
+ bucket: options.bucket,
8
+ prefix: options.prefix ?? "",
9
+ });
10
+ }
11
+ return makeStorage({
12
+ client: new GcsStorageClient(),
13
+ bucket: options.bucket,
14
+ prefix: options.prefix ?? "",
15
+ });
16
+ }
17
+ function makeStorage({ client, bucket, prefix }) {
18
+ const bucketRef = client.bucket(bucket);
19
+ function fullKey(key) {
20
+ return prefix + key;
21
+ }
22
+ return {
23
+ prefix,
24
+ async read(key) {
25
+ try {
26
+ const [buffer] = await bucketRef.file(fullKey(key)).download();
27
+ return buffer;
28
+ }
29
+ catch (error) {
30
+ if (error.code === 404)
31
+ return null;
32
+ throw error;
33
+ }
34
+ },
35
+ async readText(key) {
36
+ const buf = await this.read(key);
37
+ return buf ? buf.toString("utf-8") : null;
38
+ },
39
+ async readJson(key) {
40
+ const text = await this.readText(key);
41
+ return text ? JSON.parse(text) : null;
42
+ },
43
+ async write(key, body) {
44
+ await bucketRef.file(fullKey(key)).save(body);
45
+ },
46
+ async writeJson(key, value) {
47
+ await bucketRef.file(fullKey(key)).save(JSON.stringify(value), {
48
+ contentType: "application/json",
49
+ });
50
+ },
51
+ async writeIfAbsent(key, body) {
52
+ try {
53
+ // ifGenerationMatch:0 means "object must not exist"; on collision GCS
54
+ // returns 412 PreconditionFailed, which we translate below.
55
+ await bucketRef.file(fullKey(key)).save(body, {
56
+ preconditionOpts: { ifGenerationMatch: 0 },
57
+ });
58
+ }
59
+ catch (error) {
60
+ if (error.code === 412) {
61
+ throw new StorageConflictError(key);
62
+ }
63
+ throw error;
64
+ }
65
+ },
66
+ async exists(key) {
67
+ const [exists] = await bucketRef.file(fullKey(key)).exists();
68
+ return exists;
69
+ },
70
+ async list(listPrefix) {
71
+ // bucket.getFiles auto-paginates: for buckets > 1000 objects the SDK
72
+ // transparently issues subsequent pageToken requests internally and
73
+ // returns the accumulated list as a single-element tuple.
74
+ const [files] = await bucketRef.getFiles({
75
+ prefix: fullKey(listPrefix),
76
+ });
77
+ return files.map((f) => f.name.slice(prefix.length)).sort();
78
+ },
79
+ };
80
+ }
81
+ //# sourceMappingURL=gcs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gcs.js","sourceRoot":"","sources":["../../src/storage/gcs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Thin storage abstraction. Five operations cover everything chat-service
3
+ * needs: read / write / writeIfAbsent / exists / list. Different backends
4
+ * (local FS, S3-compatible) implement the same surface; chat-service code
5
+ * never reaches past this.
6
+ *
7
+ * `writeIfAbsent` is atomic create-only — required by the edit-draft
8
+ * version-increment-on-collision loop in `storage-keys.ts`. Each backend
9
+ * implements it with its native primitive (POSIX `O_CREAT|O_EXCL`,
10
+ * S3 `IfNoneMatch: '*'`).
11
+ */
12
+ export interface Storage {
13
+ /** Optional prefix prepended to every key. */
14
+ readonly prefix: string;
15
+ read(key: string): Promise<Buffer | null>;
16
+ readText(key: string): Promise<string | null>;
17
+ readJson<T>(key: string): Promise<T | null>;
18
+ write(key: string, body: Buffer | string): Promise<void>;
19
+ writeJson(key: string, value: unknown): Promise<void>;
20
+ /** Atomic create-only write. Throws `StorageConflictError` on collision. */
21
+ writeIfAbsent(key: string, body: Buffer | string): Promise<void>;
22
+ exists(key: string): Promise<boolean>;
23
+ list(prefix: string): Promise<string[]>;
24
+ }
25
+ /**
26
+ * Thrown by `Storage.writeIfAbsent` when an object already exists at the
27
+ * target key. Callers (e.g. `writeEditDraft`) catch this and retry with an
28
+ * incremented version number.
29
+ */
30
+ export declare class StorageConflictError extends Error {
31
+ constructor(key: string);
32
+ }
33
+ //# sourceMappingURL=interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../src/storage/interface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,WAAW,OAAO;IACtB,8CAA8C;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9C,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAE5C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtD,4EAA4E;IAC5E,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACtC,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CACzC;AAED;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,GAAG,EAAE,MAAM;CAIxB"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Thrown by `Storage.writeIfAbsent` when an object already exists at the
3
+ * target key. Callers (e.g. `writeEditDraft`) catch this and retry with an
4
+ * incremented version number.
5
+ */
6
+ export class StorageConflictError extends Error {
7
+ constructor(key) {
8
+ super(`Object already exists at key: ${key}`);
9
+ this.name = "StorageConflictError";
10
+ }
11
+ }
12
+ //# sourceMappingURL=interface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interface.js","sourceRoot":"","sources":["../../src/storage/interface.ts"],"names":[],"mappings":"AA6BA;;;;GAIG;AACH,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,GAAW;QACrB,KAAK,CAAC,iCAAiC,GAAG,EAAE,CAAC,CAAC;QAC9C,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF"}