@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,247 @@
1
+ /**
2
+ * Runtime state — what *is*, as against what should be.
3
+ *
4
+ * The split of responsibilities matters and is easy to erode:
5
+ *
6
+ * config file what exists (declared, diffable, source of truth)
7
+ * credential store credential values
8
+ * state (here) connection status, token expiry, cursors, provider state
9
+ * audit log what happened — objects in the blob store, see `#audit`
10
+ *
11
+ * Nothing here is a source of truth for what *should* exist. All of it can be
12
+ * deleted and rebuilt: reconcile restores the connection rows from the config
13
+ * file, a discovery cache re-discovers, and a logged-out connector authorises
14
+ * again. Keep it that way — the deployed target scales to zero, so
15
+ * administering it by connecting to a database is not a workable model.
16
+ *
17
+ * **There is no database.** Every access below is a point read or write on one
18
+ * key, plus one prefix listing; nothing joins, aggregates, orders, or searches.
19
+ * That was true when this was four SQL tables too — the tables were carrying a
20
+ * query engine nothing queried. It is one object per key in the `BlobStore` the
21
+ * target already opened, which means the local and deployed paths are the same
22
+ * code and the deployed target needs no second service. ADR-020 has the
23
+ * reasoning for the log; this is the same argument applied to the rest.
24
+ */
25
+
26
+ import type { BlobStore } from '../blobs/index.ts';
27
+ import { keyFromEntry, namespacePrefix, objectKey } from './keys.ts';
28
+
29
+ export type ConnectionStatus =
30
+ | 'active'
31
+ /** Declared, but its credential is missing or rejected. Does not block startup. */
32
+ | 'unauthorized'
33
+ /** No longer declared in config. Never deleted, so audit history stays meaningful. */
34
+ | 'disabled';
35
+
36
+ export interface ConnectionRecord {
37
+ readonly provider: string;
38
+ readonly id: string;
39
+ readonly displayName: string;
40
+ readonly status: ConnectionStatus;
41
+ readonly credentialExpiresAt?: Date;
42
+ readonly createdAt: Date;
43
+ readonly updatedAt: Date;
44
+ }
45
+
46
+ /** `provider.id`, e.g. `gmail.main`. The addressing form used everywhere. */
47
+ export function connectionKey(provider: string, id: string): string {
48
+ return `${provider}.${id}`;
49
+ }
50
+
51
+ export interface ConnectionRepository {
52
+ upsert(record: Omit<ConnectionRecord, 'createdAt' | 'updatedAt'>): Promise<ConnectionRecord>;
53
+ get(provider: string, id: string): Promise<ConnectionRecord | null>;
54
+ list(): Promise<ConnectionRecord[]>;
55
+ setStatus(provider: string, id: string, status: ConnectionStatus): Promise<void>;
56
+ }
57
+
58
+ /**
59
+ * Namespaced key-value, scoped by core to `<provider>/<connection>` before a
60
+ * provider ever sees it. A provider cannot read another provider's state, and
61
+ * cannot read another connection's state within its own provider.
62
+ */
63
+ export interface KeyValueStore {
64
+ get(namespace: string, key: string): Promise<string | null>;
65
+ set(namespace: string, key: string, value: string): Promise<void>;
66
+ delete(namespace: string, key: string): Promise<void>;
67
+ keys(namespace: string): Promise<string[]>;
68
+ clearNamespace(namespace: string): Promise<void>;
69
+ }
70
+
71
+ /** Sync cursors for providers that page through upstream data incrementally. */
72
+ export interface CursorRepository {
73
+ get(namespace: string): Promise<string | null>;
74
+ set(namespace: string, cursor: string): Promise<void>;
75
+ }
76
+
77
+ export interface RuntimeState {
78
+ readonly connections: ConnectionRepository;
79
+ /**
80
+ * Namespaced key-value. Named `kv` rather than `state` because the whole of
81
+ * this is state — a member called `state` on a thing called `RuntimeState`
82
+ * reads as the whole rather than the part, which is what it was doing when
83
+ * the type was still called `RuntimeState`.
84
+ */
85
+ readonly kv: KeyValueStore;
86
+ readonly cursors: CursorRepository;
87
+ }
88
+
89
+ /**
90
+ * Reserved namespaces, carrying a dot so a provider cannot collide with one.
91
+ *
92
+ * A provider's state is namespaced `<provider>/<connection>` and a provider id
93
+ * is `[a-z][a-z0-9_]*`, so a dot is a character no provider namespace can
94
+ * contain. The same rule keeps `audit.log` and `state.kv` out of reach in
95
+ * `#profile`'s layout.
96
+ */
97
+ const CONNECTIONS = 'connections.v1';
98
+ const CURSORS = 'cursors.v1';
99
+
100
+ export function createRuntimeState(
101
+ blobs: BlobStore,
102
+ now: () => Date = () => new Date(),
103
+ ): RuntimeState {
104
+ const kv = createKeyValue(blobs);
105
+
106
+ const readConnection = async (provider: string, id: string): Promise<ConnectionRecord | null> =>
107
+ decodeConnection(await kv.get(CONNECTIONS, connectionKey(provider, id)));
108
+
109
+ const connections: ConnectionRepository = {
110
+ async upsert(record) {
111
+ const existing = await readConnection(record.provider, record.id);
112
+ const stored: ConnectionRecord = {
113
+ ...record,
114
+ // Preserved across updates: when a connection was first seen is a fact
115
+ // about the connection, not about the last time reconcile ran.
116
+ createdAt: existing?.createdAt ?? now(),
117
+ updatedAt: now(),
118
+ };
119
+ await kv.set(CONNECTIONS, connectionKey(record.provider, record.id), encodeConnection(stored));
120
+ return stored;
121
+ },
122
+
123
+ get: readConnection,
124
+
125
+ async list() {
126
+ const records: ConnectionRecord[] = [];
127
+ for (const key of await kv.keys(CONNECTIONS)) {
128
+ const record = decodeConnection(await kv.get(CONNECTIONS, key));
129
+ if (record) records.push(record);
130
+ }
131
+ return records.sort(
132
+ (a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id),
133
+ );
134
+ },
135
+
136
+ async setStatus(provider, id, status) {
137
+ const existing = await readConnection(provider, id);
138
+ if (!existing) return;
139
+ await kv.set(
140
+ CONNECTIONS,
141
+ connectionKey(provider, id),
142
+ encodeConnection({ ...existing, status, updatedAt: now() }),
143
+ );
144
+ },
145
+ };
146
+
147
+ const cursors: CursorRepository = {
148
+ get: (namespace) => kv.get(CURSORS, namespace),
149
+ set: (namespace, cursor) => kv.set(CURSORS, namespace, cursor),
150
+ };
151
+
152
+ return { connections, kv, cursors };
153
+ }
154
+
155
+ /**
156
+ * One object per key.
157
+ *
158
+ * Per key rather than one document per namespace, so there is no
159
+ * read-modify-write anywhere: two writers touching different keys never
160
+ * contend, and a torn write can lose at most the one value being written.
161
+ */
162
+ export function createKeyValue(blobs: BlobStore): KeyValueStore {
163
+ return {
164
+ async get(namespace, key) {
165
+ const bytes = await blobs.get(objectKey(namespace, key));
166
+ return bytes === null ? null : new TextDecoder().decode(bytes);
167
+ },
168
+
169
+ async set(namespace, key, value) {
170
+ await blobs.put(objectKey(namespace, key), new TextEncoder().encode(value), {
171
+ contentType: 'application/json',
172
+ });
173
+ },
174
+
175
+ async delete(namespace, key) {
176
+ await blobs.delete(objectKey(namespace, key));
177
+ },
178
+
179
+ async keys(namespace) {
180
+ const prefix = namespacePrefix(namespace);
181
+ const found: string[] = [];
182
+ for (const entry of await blobs.list(prefix)) {
183
+ const key = keyFromEntry(prefix, entry.key);
184
+ if (key !== null) found.push(key);
185
+ }
186
+ return found.sort();
187
+ },
188
+
189
+ async clearNamespace(namespace) {
190
+ const prefix = namespacePrefix(namespace);
191
+ for (const entry of await blobs.list(prefix)) {
192
+ // Direct children only, matching `keys`. A nested namespace is a
193
+ // different namespace and clearing one must not empty the other.
194
+ if (keyFromEntry(prefix, entry.key) === null) continue;
195
+ await blobs.delete(entry.key);
196
+ }
197
+ },
198
+ };
199
+ }
200
+
201
+ interface StoredConnection {
202
+ provider: string;
203
+ id: string;
204
+ displayName: string;
205
+ status: string;
206
+ credentialExpiresAt?: string;
207
+ createdAt: string;
208
+ updatedAt: string;
209
+ }
210
+
211
+ function encodeConnection(record: ConnectionRecord): string {
212
+ const stored: StoredConnection = {
213
+ provider: record.provider,
214
+ id: record.id,
215
+ displayName: record.displayName,
216
+ status: record.status,
217
+ ...(record.credentialExpiresAt
218
+ ? { credentialExpiresAt: record.credentialExpiresAt.toISOString() }
219
+ : {}),
220
+ createdAt: record.createdAt.toISOString(),
221
+ updatedAt: record.updatedAt.toISOString(),
222
+ };
223
+ return JSON.stringify(stored);
224
+ }
225
+
226
+ function decodeConnection(raw: string | null): ConnectionRecord | null {
227
+ if (raw === null) return null;
228
+ try {
229
+ const stored = JSON.parse(raw) as StoredConnection;
230
+ return {
231
+ provider: stored.provider,
232
+ id: stored.id,
233
+ displayName: stored.displayName,
234
+ status: stored.status as ConnectionStatus,
235
+ ...(stored.credentialExpiresAt
236
+ ? { credentialExpiresAt: new Date(stored.credentialExpiresAt) }
237
+ : {}),
238
+ createdAt: new Date(stored.createdAt),
239
+ updatedAt: new Date(stored.updatedAt),
240
+ };
241
+ } catch {
242
+ // A record that will not parse is one nothing can use. Treating it as
243
+ // absent fails closed and lets reconcile rebuild it, which is the whole
244
+ // reason this store is allowed to be lossy.
245
+ return null;
246
+ }
247
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Turning a namespace and a key into an object key, reversibly.
3
+ *
4
+ * Both halves come from callers this module does not control. A provider
5
+ * namespace is `<provider>/<connection>` and its keys are whatever that
6
+ * provider chose; the OAuth server stores under a SHA-256 hex digest; a
7
+ * discovery cache stores under a provider id. Percent-encoding everything
8
+ * outside a small safe set is the only rule that holds for all of them.
9
+ *
10
+ * **Why not the shorter thing.** `gcp-secret-manager.ts` maps `/` to `__` and
11
+ * round-trips to refuse anything ambiguous, which is fine for refs that are
12
+ * already `[a-z0-9_-]` by schema. Keys here are not, so the same trick would
13
+ * have to refuse keys that already work today. In particular `.` must be
14
+ * escaped: a key of `..` is a directory traversal that `encodeURIComponent`
15
+ * would pass through untouched.
16
+ *
17
+ * The namespace keeps its `/` as a real path separator, so a bucket listing is
18
+ * browsable and `keys()` is one prefix list. Each *segment* is still encoded,
19
+ * so a `/` inside a segment cannot forge one.
20
+ */
21
+
22
+ const SAFE = /^[A-Za-z0-9_-]$/;
23
+
24
+ export function encodeSegment(segment: string): string {
25
+ let out = '';
26
+ for (const character of segment) {
27
+ if (SAFE.test(character)) {
28
+ out += character;
29
+ continue;
30
+ }
31
+ for (const byte of new TextEncoder().encode(character)) {
32
+ out += `%${byte.toString(16).toUpperCase().padStart(2, '0')}`;
33
+ }
34
+ }
35
+ return out;
36
+ }
37
+
38
+ export function decodeSegment(segment: string): string {
39
+ return decodeURIComponent(segment);
40
+ }
41
+
42
+ /** `oauth/tokens` becomes `oauth/tokens/`, one encoded path segment each. */
43
+ export function namespacePrefix(namespace: string): string {
44
+ if (namespace.length === 0) throw new Error('State namespace must not be empty');
45
+ return `${namespace.split('/').map(encodeSegment).join('/')}/`;
46
+ }
47
+
48
+ export function objectKey(namespace: string, key: string): string {
49
+ if (key.length === 0) throw new Error('State key must not be empty');
50
+ return `${namespacePrefix(namespace)}${encodeSegment(key)}.json`;
51
+ }
52
+
53
+ /**
54
+ * The key a listing entry came from, or `null` if it is not a direct child.
55
+ *
56
+ * Namespaces nest — `oauth` and `oauth/tokens` can both exist — so listing a
57
+ * prefix returns grandchildren too. A namespace that leaked its children's
58
+ * keys would break the isolation `KeyValueStore` exists to provide, so
59
+ * anything below the first level is skipped rather than flattened.
60
+ */
61
+ export function keyFromEntry(prefix: string, entryKey: string): string | null {
62
+ if (!entryKey.startsWith(prefix)) return null;
63
+
64
+ const remainder = entryKey.slice(prefix.length);
65
+ if (remainder.includes('/') || !remainder.endsWith('.json')) return null;
66
+
67
+ return decodeSegment(remainder.slice(0, -'.json'.length));
68
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Runtime state over an in-memory blob store, for tests.
3
+ *
4
+ * Note what this is *not*: a second implementation. It is the real
5
+ * `createRuntimeState` over `createMemoryBlobStore`, so a test cannot
6
+ * demonstrate behaviour the shipped code does not have — which is what the
7
+ * hand-written in-memory `RuntimeState` beside it used to risk, and did: its
8
+ * `list()` returned insertion order where both SQL adapters sorted.
9
+ *
10
+ * Kept behind a separate entry point anyway, so application code cannot reach
11
+ * the memory store by accident.
12
+ */
13
+
14
+ import { createMemoryBlobStore } from '../blobs/testing.ts';
15
+ import { createRuntimeState, type RuntimeState } from './index.ts';
16
+
17
+ export function createMemoryState(now: () => Date = () => new Date()): RuntimeState {
18
+ return createRuntimeState(createMemoryBlobStore(), now);
19
+ }
20
+
21
+ /** An in-memory credential store, for the same reason. */
22
+ export function createMemoryCredentials(initial: Record<string, string> = {}) {
23
+ const map = new Map(Object.entries(initial));
24
+ return {
25
+ async get(ref: string) {
26
+ return map.get(ref) ?? null;
27
+ },
28
+ async set(ref: string, value: string) {
29
+ map.set(ref, value);
30
+ },
31
+ async has(ref: string) {
32
+ return map.has(ref);
33
+ },
34
+ async delete(ref: string) {
35
+ map.delete(ref);
36
+ },
37
+ async list(prefix?: string) {
38
+ return [...map.keys()].filter((k) => !prefix || k.startsWith(prefix));
39
+ },
40
+ };
41
+ }