@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.
- package/LICENSE +202 -0
- package/README.md +144 -0
- package/bin/lanes +42 -0
- package/instructions/agents/lanes-link-scout.md +73 -0
- package/instructions/skills/lanes-link/SKILL.md +187 -0
- package/package.json +95 -0
- package/src/audit/chain.ts +278 -0
- package/src/audit/conformance.ts +374 -0
- package/src/audit/fanout.ts +97 -0
- package/src/audit/index.ts +218 -0
- package/src/audit/stdout.ts +60 -0
- package/src/auth/index.ts +220 -0
- package/src/auth/oauth/metadata.ts +75 -0
- package/src/auth/oauth/server.ts +385 -0
- package/src/auth/oauth/store.ts +215 -0
- package/src/auth/oidc.ts +206 -0
- package/src/auth/remote.ts +72 -0
- package/src/cli/argv.ts +94 -0
- package/src/cli/callback-page.ts +256 -0
- package/src/cli/commands/connect/accounts.ts +94 -0
- package/src/cli/commands/connect/authorise.ts +298 -0
- package/src/cli/commands/connect/client.ts +284 -0
- package/src/cli/commands/connect/index.ts +398 -0
- package/src/cli/commands/connect/outcome.ts +119 -0
- package/src/cli/commands/connect/requirements.ts +103 -0
- package/src/cli/commands/connect/scopes-gate.ts +146 -0
- package/src/cli/commands/connect/settle.ts +136 -0
- package/src/cli/commands/connect/setup.ts +276 -0
- package/src/cli/commands/mcp/assets.ts +189 -0
- package/src/cli/commands/mcp/harnesses.ts +143 -0
- package/src/cli/commands/mcp/list.ts +70 -0
- package/src/cli/commands/mcp/register.ts +199 -0
- package/src/cli/commands/mcp/stdio.ts +57 -0
- package/src/cli/commands/mcp.ts +22 -0
- package/src/cli/commands/operate/attach.ts +121 -0
- package/src/cli/commands/operate/audit.ts +119 -0
- package/src/cli/commands/operate/inspect.ts +355 -0
- package/src/cli/commands/operate/outputs.ts +206 -0
- package/src/cli/commands/operate/policy.ts +80 -0
- package/src/cli/commands/operate/serve.ts +55 -0
- package/src/cli/commands/operate/status.ts +133 -0
- package/src/cli/commands/operate/token.ts +70 -0
- package/src/cli/commands/operate.ts +27 -0
- package/src/cli/commands/owner/memory.ts +110 -0
- package/src/cli/commands/owner/shared.ts +125 -0
- package/src/cli/commands/owner/skills.ts +92 -0
- package/src/cli/commands/owner/vault.ts +140 -0
- package/src/cli/commands/owner.ts +46 -0
- package/src/cli/commands/profile/removal.ts +278 -0
- package/src/cli/commands/profile/remove.ts +315 -0
- package/src/cli/commands/profile.ts +156 -0
- package/src/cli/commands/secrets.ts +176 -0
- package/src/cli/commands/setup.ts +150 -0
- package/src/cli/commands/target.ts +310 -0
- package/src/cli/config-edit.ts +397 -0
- package/src/cli/endpoint-url.ts +77 -0
- package/src/cli/identity.ts +109 -0
- package/src/cli/lanes.ts +78 -0
- package/src/cli/main.ts +333 -0
- package/src/cli/oauth-error.ts +13 -0
- package/src/cli/oauth-exchange.ts +146 -0
- package/src/cli/oauth.ts +354 -0
- package/src/cli/output.ts +184 -0
- package/src/cli/prompt.ts +180 -0
- package/src/cli/publish.ts +185 -0
- package/src/cli/runtime/discovery.ts +123 -0
- package/src/cli/runtime/open.ts +342 -0
- package/src/cli/runtime/registry.ts +185 -0
- package/src/cli/runtime/select.ts +124 -0
- package/src/cli/runtime.ts +34 -0
- package/src/cli/scopes.ts +63 -0
- package/src/cli/usage.ts +111 -0
- package/src/cli/version.ts +25 -0
- package/src/connectivity/auth/README.md +36 -0
- package/src/connectivity/auth/api-key/index.ts +43 -0
- package/src/connectivity/auth/authorize.ts +49 -0
- package/src/connectivity/auth/basic/index.ts +68 -0
- package/src/connectivity/auth/bearer/index.ts +13 -0
- package/src/connectivity/auth/credential.ts +19 -0
- package/src/connectivity/auth/header/index.ts +18 -0
- package/src/connectivity/auth/index.ts +35 -0
- package/src/connectivity/auth/none/index.ts +12 -0
- package/src/connectivity/auth/oauth-authcode/broker.ts +261 -0
- package/src/connectivity/auth/oauth-authcode/index.ts +64 -0
- package/src/connectivity/auth/oauth-authcode/provider.ts +279 -0
- package/src/connectivity/auth/oauth-authcode/refresh.ts +118 -0
- package/src/connectivity/auth/resolve.ts +61 -0
- package/src/connectivity/auth/strategy/index.ts +14 -0
- package/src/connectivity/capability.ts +164 -0
- package/src/connectivity/connector.ts +175 -0
- package/src/connectivity/context.ts +77 -0
- package/src/connectivity/index.ts +94 -0
- package/src/connectivity/mail/attachments.ts +368 -0
- package/src/connectivity/mail/compose.ts +73 -0
- package/src/connectivity/mail/index.ts +40 -0
- package/src/connectivity/mail/message.ts +82 -0
- package/src/connectivity/mail/nodemailer.d.ts +83 -0
- package/src/connectivity/mail/staging.ts +143 -0
- package/src/connectivity/mail/url.ts +301 -0
- package/src/connectivity/manifest/auth.ts +135 -0
- package/src/connectivity/manifest/bundles.ts +23 -0
- package/src/connectivity/manifest/connector.ts +168 -0
- package/src/connectivity/manifest/credential-ref.ts +73 -0
- package/src/connectivity/manifest/identity.ts +43 -0
- package/src/connectivity/manifest/index.ts +46 -0
- package/src/connectivity/manifest/primitives.ts +20 -0
- package/src/connectivity/manifest/provider.ts +208 -0
- package/src/connectivity/manifest/requirements.ts +146 -0
- package/src/connectivity/manifest/setup.ts +55 -0
- package/src/connectivity/provider.ts +163 -0
- package/src/connectivity/transports/README.md +33 -0
- package/src/connectivity/transports/composite/index.ts +68 -0
- package/src/connectivity/transports/dav/calendar.ts +217 -0
- package/src/connectivity/transports/dav/capabilities.ts +151 -0
- package/src/connectivity/transports/dav/client.ts +226 -0
- package/src/connectivity/transports/dav/contacts.ts +75 -0
- package/src/connectivity/transports/dav/ical.ts +412 -0
- package/src/connectivity/transports/dav/index.ts +143 -0
- package/src/connectivity/transports/dav/operations.ts +43 -0
- package/src/connectivity/transports/dav/request.ts +161 -0
- package/src/connectivity/transports/dav/xml.ts +123 -0
- package/src/connectivity/transports/factory.ts +181 -0
- package/src/connectivity/transports/fs/capabilities.ts +88 -0
- package/src/connectivity/transports/fs/commands.ts +258 -0
- package/src/connectivity/transports/fs/index.ts +121 -0
- package/src/connectivity/transports/fs/operations.ts +45 -0
- package/src/connectivity/transports/fs/paths.ts +120 -0
- package/src/connectivity/transports/fs/result.ts +12 -0
- package/src/connectivity/transports/http/index.ts +255 -0
- package/src/connectivity/transports/imap/attachment.ts +166 -0
- package/src/connectivity/transports/imap/capabilities.ts +158 -0
- package/src/connectivity/transports/imap/client.ts +398 -0
- package/src/connectivity/transports/imap/commands.ts +385 -0
- package/src/connectivity/transports/imap/index.ts +152 -0
- package/src/connectivity/transports/imap/operations.ts +64 -0
- package/src/connectivity/transports/imap/parse.ts +130 -0
- package/src/connectivity/transports/imap/parser.ts +272 -0
- package/src/connectivity/transports/imap/result.ts +15 -0
- package/src/connectivity/transports/imap/send.ts +92 -0
- package/src/connectivity/transports/imap/socket.ts +111 -0
- package/src/connectivity/transports/imap/utf7.ts +136 -0
- package/src/connectivity/transports/index.ts +20 -0
- package/src/connectivity/transports/local/index.ts +173 -0
- package/src/connectivity/transports/mcp/index.ts +215 -0
- package/src/deployments/README.md +63 -0
- package/src/deployments/adapters/audit-blob.ts +203 -0
- package/src/deployments/adapters/filesystem.ts +184 -0
- package/src/deployments/adapters/gcp-secret-manager.ts +492 -0
- package/src/deployments/adapters/gcs.ts +191 -0
- package/src/deployments/adapters/otlp.ts +128 -0
- package/src/deployments/adapters/s3.ts +195 -0
- package/src/deployments/azure/README.md +21 -0
- package/src/deployments/bootstrap.ts +177 -0
- package/src/deployments/deploy.ts +290 -0
- package/src/deployments/driver.ts +157 -0
- package/src/deployments/drivers.ts +35 -0
- package/src/deployments/gcp/Dockerfile +70 -0
- package/src/deployments/gcp/cloudbuild.yaml +31 -0
- package/src/deployments/gcp/driver.ts +175 -0
- package/src/deployments/gcp/gcloud.ts +178 -0
- package/src/deployments/gcp/provision.ts +290 -0
- package/src/deployments/gcp/survey.ts +319 -0
- package/src/deployments/local/README.md +12 -0
- package/src/deployments/prepare.ts +257 -0
- package/src/deployments/steps.ts +137 -0
- package/src/deployments/target.ts +295 -0
- package/src/deployments/upload.ts +207 -0
- package/src/dispatch/context.ts +195 -0
- package/src/dispatch/dispatch.ts +350 -0
- package/src/dispatch/index.ts +32 -0
- package/src/dispatch/staging.ts +102 -0
- package/src/policy/index.ts +179 -0
- package/src/policy/limits.ts +77 -0
- package/src/profile/authorization.ts +81 -0
- package/src/profile/files.ts +71 -0
- package/src/profile/index.ts +76 -0
- package/src/profile/layout.ts +123 -0
- package/src/profile/load.ts +199 -0
- package/src/profile/primitives.ts +45 -0
- package/src/profile/schema.ts +347 -0
- package/src/profile/secret-detection.ts +162 -0
- package/src/profile/targets.ts +152 -0
- package/src/profile/workspace.ts +262 -0
- package/src/providers/custom/index.ts +21 -0
- package/src/providers/custom/load.ts +115 -0
- package/src/providers/custom/template.ts +156 -0
- package/src/providers/example/provider.ts +207 -0
- package/src/providers/google/calendar/index.ts +66 -0
- package/src/providers/google/calendar/redact.ts +40 -0
- package/src/providers/google/contacts/index.ts +50 -0
- package/src/providers/google/contacts/redact.ts +21 -0
- package/src/providers/google/docs/index.ts +45 -0
- package/src/providers/google/drive/hints.ts +28 -0
- package/src/providers/google/drive/index.ts +34 -0
- package/src/providers/google/drive/redact.ts +39 -0
- package/src/providers/google/drive-mcp/index.ts +21 -0
- package/src/providers/google/gmail/api.ts +42 -0
- package/src/providers/google/gmail/attachment.ts +142 -0
- package/src/providers/google/gmail/hints.ts +55 -0
- package/src/providers/google/gmail/index.ts +112 -0
- package/src/providers/google/gmail/redact.ts +56 -0
- package/src/providers/google/gmail/send.ts +365 -0
- package/src/providers/google/gmail-mcp/index.ts +35 -0
- package/src/providers/google/index.ts +10 -0
- package/src/providers/google/shared/oauth.ts +122 -0
- package/src/providers/google/shared/scopes.ts +99 -0
- package/src/providers/google/shared/setup.ts +80 -0
- package/src/providers/google/sheets/hints.ts +45 -0
- package/src/providers/google/sheets/index.ts +70 -0
- package/src/providers/google/sheets/redact.ts +45 -0
- package/src/providers/google/specs/calendar.v3.json +1829 -0
- package/src/providers/google/specs/docs.v1.json +381 -0
- package/src/providers/google/specs/drive.v3.json +2208 -0
- package/src/providers/google/specs/gmail.v1.json +2578 -0
- package/src/providers/google/specs/people.v1.json +506 -0
- package/src/providers/google/specs/sheets.v4.json +1269 -0
- package/src/providers/google/specs/tasks.v1.json +840 -0
- package/src/providers/google/specs/vendor.ts +661 -0
- package/src/providers/google/tasks/index.ts +53 -0
- package/src/providers/google/tasks/redact.ts +34 -0
- package/src/providers/harness.ts +95 -0
- package/src/providers/icloud/calendar/index.ts +27 -0
- package/src/providers/icloud/contacts/index.ts +17 -0
- package/src/providers/icloud/drive/index.ts +47 -0
- package/src/providers/icloud/index.ts +8 -0
- package/src/providers/icloud/mail/index.ts +37 -0
- package/src/providers/icloud/shared/setup.ts +66 -0
- package/src/providers/index.ts +93 -0
- package/src/providers/linear/index.ts +11 -0
- package/src/providers/linear/scopes.ts +7 -0
- package/src/providers/memory/provider.ts +429 -0
- package/src/providers/notion/index.ts +19 -0
- package/src/providers/owner.ts +49 -0
- package/src/providers/scopes.ts +26 -0
- package/src/providers/setup/plan.ts +141 -0
- package/src/providers/setup/provider.ts +323 -0
- package/src/providers/shared/frontmatter.ts +119 -0
- package/src/providers/skills/provider.ts +283 -0
- package/src/providers/skills/store.ts +252 -0
- package/src/providers/vault/provider.ts +194 -0
- package/src/registry/index.ts +36 -0
- package/src/registry/policy-bridge.ts +32 -0
- package/src/registry/reconcile.ts +313 -0
- package/src/registry/registry.ts +240 -0
- package/src/secrets/document.ts +293 -0
- package/src/secrets/index.ts +154 -0
- package/src/secrets/system.ts +173 -0
- package/src/secrets/vault.ts +336 -0
- package/src/server/attachments.ts +197 -0
- package/src/server/container.ts +96 -0
- package/src/server/edge.ts +53 -0
- package/src/server/endpoint.ts +352 -0
- package/src/server/generations.ts +362 -0
- package/src/server/harness.ts +400 -0
- package/src/server/index.ts +331 -0
- package/src/server/logging.ts +41 -0
- package/src/server/mcp/build.ts +68 -0
- package/src/server/mcp/icon.ts +145 -0
- package/src/server/mcp/index.ts +32 -0
- package/src/server/mcp/instructions.ts +245 -0
- package/src/server/mcp/naming.ts +39 -0
- package/src/server/mcp/prompts.ts +78 -0
- package/src/server/mcp/resources.ts +106 -0
- package/src/server/mcp/routing.ts +117 -0
- package/src/server/mcp/schema.ts +78 -0
- package/src/server/mcp/tools.ts +186 -0
- package/src/server/mcp/visibility.ts +132 -0
- package/src/server/oauth.ts +222 -0
- package/src/server/rebinding.ts +53 -0
- package/src/server/stdio.ts +192 -0
- package/src/stores/blobs/conformance.ts +223 -0
- package/src/stores/blobs/index.ts +125 -0
- package/src/stores/blobs/testing.ts +49 -0
- package/src/stores/state/index.ts +247 -0
- package/src/stores/state/keys.ts +68 -0
- 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
|
+
}
|