@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,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
|
+
}
|