@lanes-sh/link 0.1.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 (276) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +144 -0
  3. package/bin/lanes +42 -0
  4. package/instructions/agents/lanes-link-scout.md +73 -0
  5. package/instructions/skills/lanes-link/SKILL.md +187 -0
  6. package/package.json +95 -0
  7. package/src/audit/chain.ts +278 -0
  8. package/src/audit/conformance.ts +374 -0
  9. package/src/audit/fanout.ts +97 -0
  10. package/src/audit/index.ts +218 -0
  11. package/src/audit/stdout.ts +60 -0
  12. package/src/auth/index.ts +220 -0
  13. package/src/auth/oauth/metadata.ts +75 -0
  14. package/src/auth/oauth/server.ts +385 -0
  15. package/src/auth/oauth/store.ts +215 -0
  16. package/src/auth/oidc.ts +206 -0
  17. package/src/auth/remote.ts +72 -0
  18. package/src/cli/argv.ts +94 -0
  19. package/src/cli/callback-page.ts +256 -0
  20. package/src/cli/commands/connect/accounts.ts +94 -0
  21. package/src/cli/commands/connect/authorise.ts +298 -0
  22. package/src/cli/commands/connect/client.ts +284 -0
  23. package/src/cli/commands/connect/index.ts +398 -0
  24. package/src/cli/commands/connect/outcome.ts +119 -0
  25. package/src/cli/commands/connect/requirements.ts +103 -0
  26. package/src/cli/commands/connect/scopes-gate.ts +146 -0
  27. package/src/cli/commands/connect/settle.ts +136 -0
  28. package/src/cli/commands/connect/setup.ts +276 -0
  29. package/src/cli/commands/mcp/assets.ts +189 -0
  30. package/src/cli/commands/mcp/harnesses.ts +143 -0
  31. package/src/cli/commands/mcp/list.ts +70 -0
  32. package/src/cli/commands/mcp/register.ts +199 -0
  33. package/src/cli/commands/mcp/stdio.ts +57 -0
  34. package/src/cli/commands/mcp.ts +22 -0
  35. package/src/cli/commands/operate/attach.ts +121 -0
  36. package/src/cli/commands/operate/audit.ts +119 -0
  37. package/src/cli/commands/operate/inspect.ts +355 -0
  38. package/src/cli/commands/operate/outputs.ts +206 -0
  39. package/src/cli/commands/operate/policy.ts +80 -0
  40. package/src/cli/commands/operate/serve.ts +55 -0
  41. package/src/cli/commands/operate/status.ts +133 -0
  42. package/src/cli/commands/operate/token.ts +70 -0
  43. package/src/cli/commands/operate.ts +27 -0
  44. package/src/cli/commands/owner/memory.ts +110 -0
  45. package/src/cli/commands/owner/shared.ts +125 -0
  46. package/src/cli/commands/owner/skills.ts +92 -0
  47. package/src/cli/commands/owner/vault.ts +140 -0
  48. package/src/cli/commands/owner.ts +46 -0
  49. package/src/cli/commands/profile/removal.ts +278 -0
  50. package/src/cli/commands/profile/remove.ts +315 -0
  51. package/src/cli/commands/profile.ts +156 -0
  52. package/src/cli/commands/secrets.ts +176 -0
  53. package/src/cli/commands/setup.ts +150 -0
  54. package/src/cli/commands/target.ts +310 -0
  55. package/src/cli/config-edit.ts +397 -0
  56. package/src/cli/endpoint-url.ts +77 -0
  57. package/src/cli/identity.ts +109 -0
  58. package/src/cli/lanes.ts +78 -0
  59. package/src/cli/main.ts +333 -0
  60. package/src/cli/oauth-error.ts +13 -0
  61. package/src/cli/oauth-exchange.ts +146 -0
  62. package/src/cli/oauth.ts +354 -0
  63. package/src/cli/output.ts +184 -0
  64. package/src/cli/prompt.ts +180 -0
  65. package/src/cli/publish.ts +185 -0
  66. package/src/cli/runtime/discovery.ts +123 -0
  67. package/src/cli/runtime/open.ts +342 -0
  68. package/src/cli/runtime/registry.ts +185 -0
  69. package/src/cli/runtime/select.ts +124 -0
  70. package/src/cli/runtime.ts +34 -0
  71. package/src/cli/scopes.ts +63 -0
  72. package/src/cli/usage.ts +111 -0
  73. package/src/cli/version.ts +25 -0
  74. package/src/connectivity/auth/README.md +36 -0
  75. package/src/connectivity/auth/api-key/index.ts +43 -0
  76. package/src/connectivity/auth/authorize.ts +49 -0
  77. package/src/connectivity/auth/basic/index.ts +68 -0
  78. package/src/connectivity/auth/bearer/index.ts +13 -0
  79. package/src/connectivity/auth/credential.ts +19 -0
  80. package/src/connectivity/auth/header/index.ts +18 -0
  81. package/src/connectivity/auth/index.ts +35 -0
  82. package/src/connectivity/auth/none/index.ts +12 -0
  83. package/src/connectivity/auth/oauth-authcode/broker.ts +261 -0
  84. package/src/connectivity/auth/oauth-authcode/index.ts +64 -0
  85. package/src/connectivity/auth/oauth-authcode/provider.ts +279 -0
  86. package/src/connectivity/auth/oauth-authcode/refresh.ts +118 -0
  87. package/src/connectivity/auth/resolve.ts +61 -0
  88. package/src/connectivity/auth/strategy/index.ts +14 -0
  89. package/src/connectivity/capability.ts +164 -0
  90. package/src/connectivity/connector.ts +175 -0
  91. package/src/connectivity/context.ts +77 -0
  92. package/src/connectivity/index.ts +94 -0
  93. package/src/connectivity/mail/attachments.ts +368 -0
  94. package/src/connectivity/mail/compose.ts +73 -0
  95. package/src/connectivity/mail/index.ts +40 -0
  96. package/src/connectivity/mail/message.ts +82 -0
  97. package/src/connectivity/mail/nodemailer.d.ts +83 -0
  98. package/src/connectivity/mail/staging.ts +143 -0
  99. package/src/connectivity/mail/url.ts +301 -0
  100. package/src/connectivity/manifest/auth.ts +135 -0
  101. package/src/connectivity/manifest/bundles.ts +23 -0
  102. package/src/connectivity/manifest/connector.ts +168 -0
  103. package/src/connectivity/manifest/credential-ref.ts +73 -0
  104. package/src/connectivity/manifest/identity.ts +43 -0
  105. package/src/connectivity/manifest/index.ts +46 -0
  106. package/src/connectivity/manifest/primitives.ts +20 -0
  107. package/src/connectivity/manifest/provider.ts +208 -0
  108. package/src/connectivity/manifest/requirements.ts +146 -0
  109. package/src/connectivity/manifest/setup.ts +55 -0
  110. package/src/connectivity/provider.ts +163 -0
  111. package/src/connectivity/transports/README.md +33 -0
  112. package/src/connectivity/transports/composite/index.ts +68 -0
  113. package/src/connectivity/transports/dav/calendar.ts +217 -0
  114. package/src/connectivity/transports/dav/capabilities.ts +151 -0
  115. package/src/connectivity/transports/dav/client.ts +226 -0
  116. package/src/connectivity/transports/dav/contacts.ts +75 -0
  117. package/src/connectivity/transports/dav/ical.ts +412 -0
  118. package/src/connectivity/transports/dav/index.ts +143 -0
  119. package/src/connectivity/transports/dav/operations.ts +43 -0
  120. package/src/connectivity/transports/dav/request.ts +161 -0
  121. package/src/connectivity/transports/dav/xml.ts +123 -0
  122. package/src/connectivity/transports/factory.ts +181 -0
  123. package/src/connectivity/transports/fs/capabilities.ts +88 -0
  124. package/src/connectivity/transports/fs/commands.ts +258 -0
  125. package/src/connectivity/transports/fs/index.ts +121 -0
  126. package/src/connectivity/transports/fs/operations.ts +45 -0
  127. package/src/connectivity/transports/fs/paths.ts +120 -0
  128. package/src/connectivity/transports/fs/result.ts +12 -0
  129. package/src/connectivity/transports/http/index.ts +255 -0
  130. package/src/connectivity/transports/imap/attachment.ts +166 -0
  131. package/src/connectivity/transports/imap/capabilities.ts +158 -0
  132. package/src/connectivity/transports/imap/client.ts +398 -0
  133. package/src/connectivity/transports/imap/commands.ts +385 -0
  134. package/src/connectivity/transports/imap/index.ts +152 -0
  135. package/src/connectivity/transports/imap/operations.ts +64 -0
  136. package/src/connectivity/transports/imap/parse.ts +130 -0
  137. package/src/connectivity/transports/imap/parser.ts +272 -0
  138. package/src/connectivity/transports/imap/result.ts +15 -0
  139. package/src/connectivity/transports/imap/send.ts +92 -0
  140. package/src/connectivity/transports/imap/socket.ts +111 -0
  141. package/src/connectivity/transports/imap/utf7.ts +136 -0
  142. package/src/connectivity/transports/index.ts +20 -0
  143. package/src/connectivity/transports/local/index.ts +173 -0
  144. package/src/connectivity/transports/mcp/index.ts +215 -0
  145. package/src/deployments/README.md +63 -0
  146. package/src/deployments/adapters/audit-blob.ts +203 -0
  147. package/src/deployments/adapters/filesystem.ts +184 -0
  148. package/src/deployments/adapters/gcp-secret-manager.ts +492 -0
  149. package/src/deployments/adapters/gcs.ts +191 -0
  150. package/src/deployments/adapters/otlp.ts +128 -0
  151. package/src/deployments/adapters/s3.ts +195 -0
  152. package/src/deployments/azure/README.md +21 -0
  153. package/src/deployments/bootstrap.ts +177 -0
  154. package/src/deployments/deploy.ts +290 -0
  155. package/src/deployments/driver.ts +157 -0
  156. package/src/deployments/drivers.ts +35 -0
  157. package/src/deployments/gcp/Dockerfile +70 -0
  158. package/src/deployments/gcp/cloudbuild.yaml +31 -0
  159. package/src/deployments/gcp/driver.ts +175 -0
  160. package/src/deployments/gcp/gcloud.ts +178 -0
  161. package/src/deployments/gcp/provision.ts +290 -0
  162. package/src/deployments/gcp/survey.ts +319 -0
  163. package/src/deployments/local/README.md +12 -0
  164. package/src/deployments/prepare.ts +257 -0
  165. package/src/deployments/steps.ts +137 -0
  166. package/src/deployments/target.ts +295 -0
  167. package/src/deployments/upload.ts +207 -0
  168. package/src/dispatch/context.ts +195 -0
  169. package/src/dispatch/dispatch.ts +350 -0
  170. package/src/dispatch/index.ts +32 -0
  171. package/src/dispatch/staging.ts +102 -0
  172. package/src/policy/index.ts +179 -0
  173. package/src/policy/limits.ts +77 -0
  174. package/src/profile/authorization.ts +81 -0
  175. package/src/profile/files.ts +71 -0
  176. package/src/profile/index.ts +76 -0
  177. package/src/profile/layout.ts +123 -0
  178. package/src/profile/load.ts +199 -0
  179. package/src/profile/primitives.ts +45 -0
  180. package/src/profile/schema.ts +347 -0
  181. package/src/profile/secret-detection.ts +162 -0
  182. package/src/profile/targets.ts +152 -0
  183. package/src/profile/workspace.ts +262 -0
  184. package/src/providers/custom/index.ts +21 -0
  185. package/src/providers/custom/load.ts +115 -0
  186. package/src/providers/custom/template.ts +156 -0
  187. package/src/providers/example/provider.ts +207 -0
  188. package/src/providers/google/calendar/index.ts +66 -0
  189. package/src/providers/google/calendar/redact.ts +40 -0
  190. package/src/providers/google/contacts/index.ts +50 -0
  191. package/src/providers/google/contacts/redact.ts +21 -0
  192. package/src/providers/google/docs/index.ts +45 -0
  193. package/src/providers/google/drive/hints.ts +28 -0
  194. package/src/providers/google/drive/index.ts +34 -0
  195. package/src/providers/google/drive/redact.ts +39 -0
  196. package/src/providers/google/drive-mcp/index.ts +21 -0
  197. package/src/providers/google/gmail/api.ts +42 -0
  198. package/src/providers/google/gmail/attachment.ts +142 -0
  199. package/src/providers/google/gmail/hints.ts +55 -0
  200. package/src/providers/google/gmail/index.ts +112 -0
  201. package/src/providers/google/gmail/redact.ts +56 -0
  202. package/src/providers/google/gmail/send.ts +365 -0
  203. package/src/providers/google/gmail-mcp/index.ts +35 -0
  204. package/src/providers/google/index.ts +10 -0
  205. package/src/providers/google/shared/oauth.ts +122 -0
  206. package/src/providers/google/shared/scopes.ts +99 -0
  207. package/src/providers/google/shared/setup.ts +80 -0
  208. package/src/providers/google/sheets/hints.ts +45 -0
  209. package/src/providers/google/sheets/index.ts +70 -0
  210. package/src/providers/google/sheets/redact.ts +45 -0
  211. package/src/providers/google/specs/calendar.v3.json +1829 -0
  212. package/src/providers/google/specs/docs.v1.json +381 -0
  213. package/src/providers/google/specs/drive.v3.json +2208 -0
  214. package/src/providers/google/specs/gmail.v1.json +2578 -0
  215. package/src/providers/google/specs/people.v1.json +506 -0
  216. package/src/providers/google/specs/sheets.v4.json +1269 -0
  217. package/src/providers/google/specs/tasks.v1.json +840 -0
  218. package/src/providers/google/specs/vendor.ts +661 -0
  219. package/src/providers/google/tasks/index.ts +53 -0
  220. package/src/providers/google/tasks/redact.ts +34 -0
  221. package/src/providers/harness.ts +95 -0
  222. package/src/providers/icloud/calendar/index.ts +27 -0
  223. package/src/providers/icloud/contacts/index.ts +17 -0
  224. package/src/providers/icloud/drive/index.ts +47 -0
  225. package/src/providers/icloud/index.ts +8 -0
  226. package/src/providers/icloud/mail/index.ts +37 -0
  227. package/src/providers/icloud/shared/setup.ts +66 -0
  228. package/src/providers/index.ts +93 -0
  229. package/src/providers/linear/index.ts +11 -0
  230. package/src/providers/linear/scopes.ts +7 -0
  231. package/src/providers/memory/provider.ts +429 -0
  232. package/src/providers/notion/index.ts +19 -0
  233. package/src/providers/owner.ts +49 -0
  234. package/src/providers/scopes.ts +26 -0
  235. package/src/providers/setup/plan.ts +141 -0
  236. package/src/providers/setup/provider.ts +323 -0
  237. package/src/providers/shared/frontmatter.ts +119 -0
  238. package/src/providers/skills/provider.ts +283 -0
  239. package/src/providers/skills/store.ts +252 -0
  240. package/src/providers/vault/provider.ts +194 -0
  241. package/src/registry/index.ts +36 -0
  242. package/src/registry/policy-bridge.ts +32 -0
  243. package/src/registry/reconcile.ts +313 -0
  244. package/src/registry/registry.ts +240 -0
  245. package/src/secrets/document.ts +293 -0
  246. package/src/secrets/index.ts +154 -0
  247. package/src/secrets/system.ts +173 -0
  248. package/src/secrets/vault.ts +336 -0
  249. package/src/server/attachments.ts +197 -0
  250. package/src/server/container.ts +96 -0
  251. package/src/server/edge.ts +53 -0
  252. package/src/server/endpoint.ts +352 -0
  253. package/src/server/generations.ts +362 -0
  254. package/src/server/harness.ts +400 -0
  255. package/src/server/index.ts +331 -0
  256. package/src/server/logging.ts +41 -0
  257. package/src/server/mcp/build.ts +68 -0
  258. package/src/server/mcp/icon.ts +145 -0
  259. package/src/server/mcp/index.ts +32 -0
  260. package/src/server/mcp/instructions.ts +245 -0
  261. package/src/server/mcp/naming.ts +39 -0
  262. package/src/server/mcp/prompts.ts +78 -0
  263. package/src/server/mcp/resources.ts +106 -0
  264. package/src/server/mcp/routing.ts +117 -0
  265. package/src/server/mcp/schema.ts +78 -0
  266. package/src/server/mcp/tools.ts +186 -0
  267. package/src/server/mcp/visibility.ts +132 -0
  268. package/src/server/oauth.ts +222 -0
  269. package/src/server/rebinding.ts +53 -0
  270. package/src/server/stdio.ts +192 -0
  271. package/src/stores/blobs/conformance.ts +223 -0
  272. package/src/stores/blobs/index.ts +125 -0
  273. package/src/stores/blobs/testing.ts +49 -0
  274. package/src/stores/state/index.ts +247 -0
  275. package/src/stores/state/keys.ts +68 -0
  276. package/src/stores/state/testing.ts +41 -0
@@ -0,0 +1,203 @@
1
+ import {
2
+ auditKey,
3
+ decodeEvent,
4
+ encodeEvent,
5
+ hashBytes,
6
+ newRunId,
7
+ stampOf,
8
+ runMarkerKey,
9
+ verifyChain,
10
+ type AuditDraft,
11
+ type AuditEvent,
12
+ type AuditQuery,
13
+ type AuditStore,
14
+ type AuditVerification,
15
+ type ChainedEvent,
16
+ type RunMarker,
17
+ type StoredRecord,
18
+ } from '#audit';
19
+ import type { BlobStore } from '#stores/blobs';
20
+
21
+ /**
22
+ * The audit log as objects in a blob store.
23
+ *
24
+ * One event is one object. That is the same layout locally and deployed — a
25
+ * directory under the profile and a prefix in the bucket are the same tree —
26
+ * so there is one sink, one reader, and no question about whether the two
27
+ * behave alike.
28
+ *
29
+ * **Why objects rather than one appended file.** ADR-013 rejected file-backed
30
+ * audit on the grounds that "a log file cannot enforce that at all; anything
31
+ * that can open the file can rewrite it". True of one mutable file, and not
32
+ * true of a set of immutable objects: a bucket retention policy or an
33
+ * object hold makes each event genuinely unwritable, enforced by the platform
34
+ * rather than by this interface declining to offer an `update`. Appending to
35
+ * an object is also not a thing object storage does, so the alternative was a
36
+ * read-modify-write of the whole log per call.
37
+ *
38
+ * The cost, stated rather than discovered later: an event is a few hundred
39
+ * bytes against a filesystem block of four kilobytes, so locally this spends
40
+ * roughly five to ten times the space an appended file would. Retention is the
41
+ * answer when that matters, and `#audit` is explicit that it belongs in an
42
+ * operator-run command outside this interface — or, deployed, in a bucket
43
+ * lifecycle rule that needs no code at all.
44
+ */
45
+
46
+ /**
47
+ * How far back `tail` will look for its newest N.
48
+ *
49
+ * A bound is needed because finding the newest keys without enumerating every
50
+ * key means guessing where to look. One list per year is cheap and returns
51
+ * keys without reading a single object, so ten of them costs little even when
52
+ * the log is empty.
53
+ *
54
+ * `tail` is a tail: returning fewer than asked because the log is older than
55
+ * this window is the correct answer, not a silent truncation of a search.
56
+ * `verify` does not use this — it enumerates everything, because it must.
57
+ */
58
+ const TAIL_YEAR_WINDOW = 10;
59
+
60
+ const MARKER_PREFIX = 'runs.closed/';
61
+
62
+ export interface BlobAuditOptions {
63
+ /** Scoped to the audit root — `data/<profile>/audit.log` or its bucket prefix. */
64
+ readonly storage: BlobStore;
65
+ readonly now?: () => Date;
66
+ /** Overridable so a test can assert a chain across a known run id. */
67
+ readonly run?: string;
68
+ }
69
+
70
+ export function createBlobAuditStore(options: BlobAuditOptions): AuditStore {
71
+ const storage = options.storage;
72
+ const now = options.now ?? ((): Date => new Date());
73
+ const run = options.run ?? newRunId();
74
+
75
+ let seq = 0;
76
+ let lastHash: string | null = null;
77
+
78
+ return {
79
+ async append(draft: AuditDraft): Promise<AuditEvent> {
80
+ const event: AuditEvent = { ...draft, id: `evt_${crypto.randomUUID()}`, timestamp: now() };
81
+
82
+ // Every read and write of the chain state happens before the first
83
+ // await, so two overlapping appends still get consecutive `seq` values
84
+ // and correctly linked hashes. Objects may then land out of order, which
85
+ // does not matter: the chain is ordered by `seq`, not by arrival.
86
+ const bytes = encodeEvent({ ...event, run, seq, prev: lastHash });
87
+ seq += 1;
88
+ lastHash = hashBytes(bytes);
89
+
90
+ await storage.put(auditKey(event), bytes, { contentType: 'application/json' });
91
+ return event;
92
+ },
93
+
94
+ async close(): Promise<void> {
95
+ // A run that wrote nothing has no tail to truncate, so it needs no marker.
96
+ if (lastHash === null) return;
97
+
98
+ const marker: RunMarker = { run, events: seq, last: lastHash };
99
+ await storage.put(
100
+ runMarkerKey(run),
101
+ new TextEncoder().encode(JSON.stringify(marker)),
102
+ { contentType: 'application/json' },
103
+ );
104
+ },
105
+
106
+ async tail(query: AuditQuery = {}): Promise<AuditEvent[]> {
107
+ const limit = query.limit ?? 50;
108
+ const found: ChainedEvent[] = [];
109
+
110
+ const start = now().getUTCFullYear();
111
+ const floor = query.since ? query.since.getUTCFullYear() : start - TAIL_YEAR_WINDOW;
112
+
113
+ for (let year = start; year >= floor && found.length < limit; year -= 1) {
114
+ const keys = (await storage.list(`${year}/`))
115
+ .map((entry) => entry.key)
116
+ .filter((key) => !key.startsWith(MARKER_PREFIX))
117
+ // Keys are compact ISO within a day and the day is in the path, so
118
+ // lexicographic order is chronological — no parsing to sort.
119
+ .sort((a, b) => (a < b ? 1 : a > b ? -1 : 0));
120
+
121
+ let lastStamp: string | null = null;
122
+
123
+ for (const key of keys) {
124
+ const stamp = stampOf(key);
125
+
126
+ // Stop at the limit, but never mid-millisecond. Events sharing a
127
+ // timestamp sort by the random half of their id, so cutting inside
128
+ // one of those groups would keep an arbitrary subset of it — which
129
+ // is how a rate-limited call, answered in well under a millisecond,
130
+ // goes missing from the tail that exists to show it.
131
+ if (found.length >= limit && stamp !== lastStamp) break;
132
+ lastStamp = stamp;
133
+
134
+ const bytes = await storage.get(key);
135
+ if (bytes === null) continue;
136
+
137
+ const record = decodeEvent(bytes);
138
+ if (record && matches(record, query)) found.push(record);
139
+ }
140
+ }
141
+
142
+ // `seq` is the tiebreaker the key cannot carry — the same job SQLite's
143
+ // implicit `rowid` did. Within one writer it is the true order; across
144
+ // two writers in the same millisecond there is no true order to find.
145
+ found.sort(
146
+ (a, b) => b.timestamp.getTime() - a.timestamp.getTime() || b.seq - a.seq,
147
+ );
148
+
149
+ // Collected newest-first so the limit keeps the most recent; presented
150
+ // oldest-first, which is how a log is read.
151
+ return found.slice(0, limit).reverse().map(strip);
152
+ },
153
+
154
+ async verify(): Promise<AuditVerification> {
155
+ // Deliberately a full enumeration, unlike `tail`. A verification that
156
+ // skipped part of the log would report `ok` for a range it never looked
157
+ // at, and a chain is only checkable while it is contiguous — so there is
158
+ // no range to narrow it by even if that were wanted.
159
+ const records: StoredRecord[] = [];
160
+ const markers: RunMarker[] = [];
161
+
162
+ for (const entry of await storage.list('')) {
163
+ const bytes = await storage.get(entry.key);
164
+ if (bytes === null) continue;
165
+
166
+ if (entry.key.startsWith(MARKER_PREFIX)) {
167
+ const marker = readMarker(bytes);
168
+ if (marker) markers.push(marker);
169
+ continue;
170
+ }
171
+ records.push({ key: entry.key, bytes });
172
+ }
173
+
174
+ return verifyChain(records, markers);
175
+ },
176
+ };
177
+ }
178
+
179
+ function strip(record: ChainedEvent): AuditEvent {
180
+ const { run: _run, seq: _seq, prev: _prev, ...event } = record;
181
+ return event;
182
+ }
183
+
184
+ function matches(event: AuditEvent, query: AuditQuery): boolean {
185
+ if (query.since && event.timestamp < query.since) return false;
186
+ if (query.provider && event.provider !== query.provider) return false;
187
+ if (query.connection && event.connection !== query.connection) return false;
188
+ if (query.capability && event.capability !== query.capability) return false;
189
+ if (query.deniedOnly && event.authorization === 'allowed') return false;
190
+ return true;
191
+ }
192
+
193
+ function readMarker(bytes: Uint8Array): RunMarker | null {
194
+ try {
195
+ const parsed = JSON.parse(new TextDecoder().decode(bytes)) as Record<string, unknown>;
196
+ if (typeof parsed['run'] !== 'string') return null;
197
+ if (typeof parsed['events'] !== 'number') return null;
198
+ if (typeof parsed['last'] !== 'string') return null;
199
+ return { run: parsed['run'], events: parsed['events'], last: parsed['last'] };
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
@@ -0,0 +1,184 @@
1
+ import { mkdir, readdir, readFile, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { dirname, join, relative, resolve, sep } from 'node:path';
4
+ import type { BlobKey, BlobMetadata, BlobStore } from '#stores/blobs';
5
+
6
+ /**
7
+ * Filesystem blob store — the `local` target's adapter.
8
+ *
9
+ * Nothing consumes this in M1 or M2: the example and Gmail read providers store
10
+ * no bytes. It exists because the owner layer (M3 — memory attachments, vault
11
+ * documents) is the workload the interface was defined for, and defining
12
+ * storage after providers exist is more expensive than before.
13
+ *
14
+ * Do not add S3, Azure, or GCS adapters until a provider actually writes bytes.
15
+ */
16
+
17
+ export interface FilesystemBlobStoreOptions {
18
+ readonly root: string;
19
+ }
20
+
21
+ export function createFilesystemBlobStore(options: FilesystemBlobStoreOptions): BlobStore {
22
+ const root = resolve(options.root);
23
+
24
+ /**
25
+ * Remove directories a delete has just emptied, up to but never including
26
+ * the root.
27
+ *
28
+ * An object store has no directories: `a/b/c.txt` is one flat key, and
29
+ * removing it leaves nothing called `a/b`. This adapter emulates that
30
+ * interface, so a surviving empty directory is a filesystem artifact leaking
31
+ * through it — and the one that matters is `data/<profile>`, which would
32
+ * otherwise outlive the profile whose objects it held.
33
+ *
34
+ * `rmdir` rather than a recursive remove, and the error swallowed: it fails
35
+ * when the directory is not empty, which is exactly the signal to stop.
36
+ */
37
+ const pruneEmptyParents = async (directory: string): Promise<void> => {
38
+ let current = directory;
39
+ while (current.startsWith(root) && current !== root) {
40
+ try {
41
+ await rmdir(current);
42
+ } catch {
43
+ return;
44
+ }
45
+ current = dirname(current);
46
+ }
47
+ };
48
+
49
+ /**
50
+ * Resolve a key to an absolute path, refusing anything that lands outside
51
+ * the root.
52
+ *
53
+ * `scopeBlobStore` already rejects traversal before a key reaches here, but
54
+ * this adapter is also usable directly, and a containment check belongs at
55
+ * the point where a path actually becomes a filesystem operation. Defence in
56
+ * depth is cheap; a provider escaping its directory is not.
57
+ */
58
+ const pathFor = (key: BlobKey): string => {
59
+ const resolved = resolve(root, key);
60
+ const rel = relative(root, resolved);
61
+
62
+ if (rel === '' || rel.startsWith('..') || rel.startsWith(`..${sep}`)) {
63
+ throw new Error(`Blob key resolves outside the store root: ${key}`);
64
+ }
65
+ return resolved;
66
+ };
67
+
68
+ return {
69
+ async put(key, data, blobOptions) {
70
+ const path = pathFor(key);
71
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
72
+
73
+ // Write-then-rename so a reader never observes a half-written blob, and
74
+ // 0600 from the moment of creation rather than a chmod afterwards, which
75
+ // would leave a window where the bytes are readable.
76
+ //
77
+ // The credential store and the vault already do this; what lands here is
78
+ // everything else a profile owns — the audit log, `state.kv`, memory, and
79
+ // cached mail attachments. Left at the umask default those are 0644 in a
80
+ // 0755 directory, which on a shared machine is every local user's to read.
81
+ const temporary = `${path}.${process.pid}.tmp`;
82
+ await writeFile(temporary, data, { mode: 0o600 });
83
+ await rename(temporary, path);
84
+
85
+ // A sidecar only when the extension cannot say it. Every blob written
86
+ // today is Markdown, JSON, or text, so in practice none is written at all
87
+ // — a `note.md` beside a `note.md.meta` recording `text/markdown` was
88
+ // noise in a directory the operator is meant to be able to read.
89
+ if (blobOptions?.contentType && inferContentType(path) !== blobOptions.contentType) {
90
+ await writeFile(`${path}.meta`, JSON.stringify({ contentType: blobOptions.contentType }), {
91
+ mode: 0o600,
92
+ });
93
+ } else {
94
+ // A rewrite may be narrowing a type the extension now covers.
95
+ await rm(`${path}.meta`, { force: true });
96
+ }
97
+ },
98
+
99
+ async get(key) {
100
+ const path = pathFor(key);
101
+ if (!existsSync(path)) return null;
102
+ return new Uint8Array(await readFile(path));
103
+ },
104
+
105
+ async has(key) {
106
+ return existsSync(pathFor(key));
107
+ },
108
+
109
+ async delete(key) {
110
+ const path = pathFor(key);
111
+ await rm(path, { force: true });
112
+ await rm(`${path}.meta`, { force: true });
113
+ await pruneEmptyParents(dirname(path));
114
+ },
115
+
116
+ async list(prefix) {
117
+ const results: BlobMetadata[] = [];
118
+
119
+ const walk = async (directory: string): Promise<void> => {
120
+ let entries;
121
+ try {
122
+ entries = await readdir(directory, { withFileTypes: true });
123
+ } catch {
124
+ return; // Nothing stored yet.
125
+ }
126
+
127
+ for (const entry of entries) {
128
+ const absolute = join(directory, entry.name);
129
+ if (entry.isDirectory()) {
130
+ await walk(absolute);
131
+ continue;
132
+ }
133
+ if (entry.name.endsWith('.meta') || entry.name.endsWith('.tmp')) continue;
134
+
135
+ const key = relative(root, absolute).split(sep).join('/');
136
+ if (prefix && !key.startsWith(prefix)) continue;
137
+
138
+ const info = await stat(absolute);
139
+ const contentType = await readContentType(absolute);
140
+ results.push({
141
+ key,
142
+ size: info.size,
143
+ modifiedAt: info.mtime,
144
+ ...(contentType ? { contentType } : {}),
145
+ });
146
+ }
147
+ };
148
+
149
+ await walk(root);
150
+ return results.sort((a, b) => a.key.localeCompare(b.key));
151
+ },
152
+ };
153
+ }
154
+
155
+ /**
156
+ * What the extension says, for the handful of types anything here stores.
157
+ *
158
+ * Deliberately short: this is not a MIME database, it is the set of things a
159
+ * provider writes, and anything outside it falls back to a sidecar.
160
+ */
161
+ const BY_EXTENSION: Record<string, string> = {
162
+ '.md': 'text/markdown',
163
+ '.json': 'application/json',
164
+ '.txt': 'text/plain',
165
+ '.yaml': 'application/yaml',
166
+ '.yml': 'application/yaml',
167
+ };
168
+
169
+ function inferContentType(path: string): string | undefined {
170
+ const dot = path.lastIndexOf('.');
171
+ return dot === -1 ? undefined : BY_EXTENSION[path.slice(dot).toLowerCase()];
172
+ }
173
+
174
+ async function readContentType(path: string): Promise<string | undefined> {
175
+ const inferred = inferContentType(path);
176
+ if (inferred) return inferred;
177
+
178
+ try {
179
+ const parsed = JSON.parse(await readFile(`${path}.meta`, 'utf8')) as { contentType?: string };
180
+ return parsed.contentType;
181
+ } catch {
182
+ return undefined;
183
+ }
184
+ }