@lanes-sh/link 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -8
- package/instructions/skills/lanes-link/SKILL.md +42 -14
- package/package.json +1 -1
- package/src/cli/argv.ts +50 -0
- package/src/cli/brand.ts +178 -0
- package/src/cli/callback-page.ts +108 -128
- package/src/cli/commands/connect/accounts.ts +5 -0
- package/src/cli/commands/connect/assertion.ts +187 -0
- package/src/cli/commands/connect/authorise.ts +61 -17
- package/src/cli/commands/connect/client.ts +37 -9
- package/src/cli/commands/connect/discover.ts +94 -0
- package/src/cli/commands/connect/family.ts +72 -0
- package/src/cli/commands/connect/index.ts +113 -115
- package/src/cli/commands/connect/method.ts +237 -0
- package/src/cli/commands/connect/outcome.ts +42 -1
- package/src/cli/commands/connect/pasted-token.ts +66 -0
- package/src/cli/commands/connect/requirements.ts +60 -8
- package/src/cli/commands/connect/setup.ts +16 -5
- package/src/cli/commands/connect/target-note.ts +34 -0
- package/src/cli/commands/identity.ts +258 -0
- package/src/cli/commands/knowledge/index.ts +390 -0
- package/src/cli/commands/knowledge/migrate.ts +180 -0
- package/src/cli/commands/knowledge/setup.ts +144 -0
- package/src/cli/commands/knowledge.ts +10 -0
- package/src/cli/commands/mcp/harnesses.ts +16 -2
- package/src/cli/commands/mcp/register.ts +9 -1
- package/src/cli/commands/mcp/stdio.ts +21 -0
- package/src/cli/commands/operate/dashboard.ts +107 -0
- package/src/cli/commands/operate/findings.ts +151 -0
- package/src/cli/commands/operate/inspect.ts +63 -164
- package/src/cli/commands/operate/outputs.ts +38 -11
- package/src/cli/commands/operate/policy.ts +7 -7
- package/src/cli/commands/operate/serve.ts +3 -0
- package/src/cli/commands/operate/status.ts +108 -1
- package/src/cli/commands/operate/token.ts +1 -1
- package/src/cli/commands/operate.ts +2 -0
- package/src/cli/commands/profile/declare.ts +154 -0
- package/src/cli/commands/profile/removal.ts +17 -0
- package/src/cli/commands/profile/remove.ts +5 -5
- package/src/cli/commands/profile.ts +83 -35
- package/src/cli/commands/secrets.ts +6 -6
- package/src/cli/commands/setup.ts +22 -6
- package/src/cli/commands/sync.ts +262 -0
- package/src/cli/commands/target.ts +65 -83
- package/src/cli/config-edit.ts +53 -144
- package/src/cli/config-repair.ts +186 -0
- package/src/cli/dashboard-page.ts +284 -0
- package/src/cli/dashboard-shell.ts +125 -0
- package/src/cli/dispatch-owner.ts +93 -0
- package/src/cli/identity.ts +12 -1
- package/src/cli/main.ts +90 -61
- package/src/cli/nearest.ts +45 -0
- package/src/cli/oauth-callback.ts +187 -0
- package/src/cli/oauth-exchange.ts +57 -15
- package/src/cli/oauth.ts +67 -177
- package/src/cli/output.ts +21 -5
- package/src/cli/provider-marks.ts +45 -0
- package/src/cli/runtime/open.ts +74 -51
- package/src/cli/runtime/registry.ts +60 -2
- package/src/cli/runtime/select.ts +26 -13
- package/src/cli/runtime/vault.ts +61 -0
- package/src/cli/runtime.ts +2 -1
- package/src/cli/selection.ts +365 -0
- package/src/cli/usage.ts +42 -11
- package/src/connectivity/auth/README.md +7 -1
- package/src/connectivity/auth/basic/index.ts +1 -1
- package/src/connectivity/auth/index.ts +14 -0
- package/src/connectivity/auth/oauth-authcode/broker.ts +26 -0
- package/src/connectivity/auth/oauth-authcode/index.ts +16 -2
- package/src/connectivity/auth/oauth-authcode/provider.ts +1 -1
- package/src/connectivity/auth/oauth-authcode/refresh.ts +3 -3
- package/src/connectivity/auth/oauth-jwt/README.md +33 -0
- package/src/connectivity/auth/oauth-jwt/index.ts +237 -0
- package/src/connectivity/auth/oauth-jwt/key.ts +148 -0
- package/src/connectivity/auth/resolve.ts +1 -1
- package/src/connectivity/auth/token.ts +11 -0
- package/src/connectivity/index.ts +2 -0
- package/src/connectivity/manifest/auth.ts +99 -2
- package/src/connectivity/manifest/identity.ts +12 -0
- package/src/connectivity/manifest/index.ts +3 -1
- package/src/connectivity/manifest/provider.ts +37 -8
- package/src/connectivity/manifest/requirements.ts +109 -6
- package/src/deployments/adapters/filesystem.ts +10 -1
- package/src/deployments/adapters/github-api.ts +106 -0
- package/src/deployments/adapters/github-commit.ts +103 -0
- package/src/deployments/adapters/github-repo.ts +356 -0
- package/src/deployments/adapters/github-testing.ts +258 -0
- package/src/deployments/adapters/github.ts +125 -0
- package/src/deployments/deploy.ts +94 -114
- package/src/deployments/discover.ts +103 -0
- package/src/deployments/driver.ts +8 -1
- package/src/deployments/gcp/driver.ts +3 -1
- package/src/deployments/knowledge.ts +119 -0
- package/src/deployments/prepare.ts +12 -6
- package/src/deployments/report.ts +117 -0
- package/src/deployments/servable.ts +82 -0
- package/src/deployments/serving.ts +165 -0
- package/src/deployments/sync-apply.ts +276 -0
- package/src/deployments/sync.ts +136 -0
- package/src/deployments/target.ts +3 -2
- package/src/deployments/upload.ts +19 -12
- package/src/dispatch/dispatch.ts +1 -1
- package/src/profile/deployments.ts +80 -0
- package/src/profile/identity.ts +60 -0
- package/src/profile/index.ts +23 -5
- package/src/profile/knowledge.ts +124 -0
- package/src/profile/load.ts +17 -5
- package/src/profile/primitives.ts +24 -1
- package/src/profile/schema.ts +81 -3
- package/src/profile/targets.ts +122 -109
- package/src/profile/workspace.ts +139 -79
- package/src/providers/google/calendar/index.ts +2 -0
- package/src/providers/google/contacts/index.ts +2 -0
- package/src/providers/google/docs/index.ts +2 -0
- package/src/providers/google/drive/index.ts +2 -0
- package/src/providers/google/gmail/index.ts +2 -0
- package/src/providers/google/gmail-imap/index.ts +125 -0
- package/src/providers/google/index.ts +2 -1
- package/src/providers/google/shared/oauth.ts +18 -6
- package/src/providers/google/shared/service-account.ts +110 -0
- package/src/providers/google/shared/setup.ts +21 -3
- package/src/providers/google/sheets/index.ts +2 -0
- package/src/providers/google/tasks/index.ts +2 -0
- package/src/providers/identity/provider.ts +166 -0
- package/src/providers/index.ts +3 -0
- package/src/providers/owner.ts +10 -2
- package/src/providers/scopes.ts +2 -0
- package/src/providers/setup/plan.ts +31 -9
- package/src/providers/setup/provider.ts +23 -0
- package/src/providers/slack/index.ts +81 -33
- package/src/providers/slack/oauth.ts +103 -0
- package/src/providers/slack/scopes.ts +37 -0
- package/src/server/container.ts +18 -1
- package/src/server/cors.ts +252 -0
- package/src/server/dashboard.ts +208 -0
- package/src/server/endpoint.ts +45 -1
- package/src/server/generations.ts +11 -2
- package/src/server/harness.ts +7 -0
- package/src/server/index.ts +45 -6
- package/src/server/mcp/index.ts +1 -0
- package/src/server/mcp/instructions.ts +28 -1
- package/src/server/mcp/visibility.ts +33 -0
- package/src/stores/blobs/route.ts +123 -0
package/src/server/index.ts
CHANGED
|
@@ -3,6 +3,13 @@ import type { Logger } from '#connectivity';
|
|
|
3
3
|
import { capabilityIdForToolName } from '#server/mcp';
|
|
4
4
|
import { ATTACHMENTS_PATH, stageAttachment } from './attachments.ts';
|
|
5
5
|
import { allowedHostnamesFor, rebindingRefusal } from './rebinding.ts';
|
|
6
|
+
import { ANY_ORIGIN, corsAware, type CorsPolicy } from './cors.ts';
|
|
7
|
+
import {
|
|
8
|
+
DASHBOARD_PATH,
|
|
9
|
+
dashboardSessions,
|
|
10
|
+
handleDashboard,
|
|
11
|
+
servesDashboard,
|
|
12
|
+
} from './dashboard.ts';
|
|
6
13
|
import type { Generation } from './generation.ts';
|
|
7
14
|
import type { Generations } from './generations.ts';
|
|
8
15
|
import { callerKey, failedAuthLimiter, FAILED_AUTH_PER_MINUTE, tooManyAttempts } from './edge.ts';
|
|
@@ -49,6 +56,15 @@ export interface ServerOptions {
|
|
|
49
56
|
readonly authorization?: AuthorizationSurface | undefined;
|
|
50
57
|
/** Hostnames this endpoint answers to. See `./rebinding.ts`. */
|
|
51
58
|
readonly allowedHostnames?: readonly string[] | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Serve the dashboard.
|
|
61
|
+
*
|
|
62
|
+
* Off unless asked for, and `serve()` withholds it anyway when the bind
|
|
63
|
+
* address is not loopback. `lanes link start` asks; `container.ts` does not.
|
|
64
|
+
* See `./dashboard.ts` for why a deployed instance has no browser-shaped door
|
|
65
|
+
* to put it behind.
|
|
66
|
+
*/
|
|
67
|
+
readonly dashboard?: boolean | undefined;
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
type RefusalReason = Extract<AuthOutcome, { ok: false }>['reason'];
|
|
@@ -110,6 +126,7 @@ export interface RequestHandler {
|
|
|
110
126
|
export function createRequestHandler(options: ServerOptions): RequestHandler {
|
|
111
127
|
let probedAt = 0;
|
|
112
128
|
const failedAuth = failedAuthLimiter();
|
|
129
|
+
const sessions = dashboardSessions();
|
|
113
130
|
|
|
114
131
|
/**
|
|
115
132
|
* Re-read the config because a call named a tool we do not serve.
|
|
@@ -169,6 +186,19 @@ export function createRequestHandler(options: ServerOptions): RequestHandler {
|
|
|
169
186
|
});
|
|
170
187
|
}
|
|
171
188
|
|
|
189
|
+
// Above the 404 gate and outside the bearer path below, because a
|
|
190
|
+
// top-level browser navigation carries no `Authorization` header — so it
|
|
191
|
+
// authenticates itself, against the same authenticator. Unset, this is
|
|
192
|
+
// never reached and `/dashboard` is a 404 like any other unknown path.
|
|
193
|
+
if (options.dashboard && url.pathname === DASHBOARD_PATH) {
|
|
194
|
+
return await handleDashboard(request, {
|
|
195
|
+
generations: options.generations,
|
|
196
|
+
authenticator: options.authenticator,
|
|
197
|
+
primary: options.primary,
|
|
198
|
+
sessions,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
172
202
|
if (
|
|
173
203
|
url.pathname !== MCP_PATH &&
|
|
174
204
|
url.pathname !== ATTACHMENTS_PATH &&
|
|
@@ -335,17 +365,26 @@ export function serve(options: ServeOptions): RunningServer {
|
|
|
335
365
|
const host = options.host ?? primary.config.instance.host;
|
|
336
366
|
const port = options.port ?? primary.config.instance.port;
|
|
337
367
|
|
|
338
|
-
const
|
|
368
|
+
const loopback = isLoopback(host);
|
|
369
|
+
const allowedHostnames = options.allowedHostnames ?? allowedHostnamesFor(host, loopback);
|
|
370
|
+
|
|
371
|
+
// Cross-origin access, and its absence, are decided here for the same reason
|
|
372
|
+
// `allowedHostnames` and `dashboard` are: they are all properties of what this
|
|
373
|
+
// is bound to. The two are mutually exclusive and the exclusion is the
|
|
374
|
+
// decision — see `./cors.ts`, and ADR-039.
|
|
375
|
+
const cors: CorsPolicy | undefined = loopback
|
|
376
|
+
? undefined
|
|
377
|
+
: { allowedOrigins: primary.config.auth.allowed_origins ?? [ANY_ORIGIN] };
|
|
339
378
|
const handler = createRequestHandler({
|
|
340
379
|
...options,
|
|
380
|
+
dashboard: servesDashboard(options.dashboard, loopback),
|
|
341
381
|
...(allowedHostnames ? { allowedHostnames } : {}),
|
|
342
382
|
});
|
|
343
383
|
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
});
|
|
384
|
+
const route = (request: Request): Promise<Response> => handler.fetch(request);
|
|
385
|
+
const fetch = cors ? corsAware(route, [MCP_PATH, ATTACHMENTS_PATH], cors) : route;
|
|
386
|
+
|
|
387
|
+
const server = Bun.serve({ hostname: host, port, fetch });
|
|
349
388
|
|
|
350
389
|
return {
|
|
351
390
|
url: `http://${host}:${port}${MCP_PATH}`,
|
package/src/server/mcp/index.ts
CHANGED
|
@@ -87,6 +87,23 @@ const SETUP = `**What is set up is answerable.** Before saying something cannot
|
|
|
87
87
|
that an account must be added, call \`setup_overview\` — then \`setup_provider\`
|
|
88
88
|
for the exact command. Running it is the owner's to do; inventing it is not.`;
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The one about not signing as the wrong person.
|
|
92
|
+
*
|
|
93
|
+
* It carries no names, and that is deliberate rather than thrift. Inlining the
|
|
94
|
+
* declaration would put a per-profile list into a string with a fixed ceiling —
|
|
95
|
+
* so the workspace with the most identities to keep straight is exactly the one
|
|
96
|
+
* whose list would be summarised away first. A pointer costs the same for one
|
|
97
|
+
* profile as for twenty, and `identity_list` has room to say when each applies,
|
|
98
|
+
* which is the half that actually prevents the mistake.
|
|
99
|
+
*
|
|
100
|
+
* Conditional like the rest: a profile that declares nothing has no `identity`
|
|
101
|
+
* connection, so the capability is unreachable and this paragraph is unspent.
|
|
102
|
+
*/
|
|
103
|
+
const IDENTITY = `**Identity is declared, not inferred.** Where a name, address or handle of the
|
|
104
|
+
owner's is needed, call \`identity_list\`: a profile may hold several, each with a
|
|
105
|
+
note on when it applies.`;
|
|
106
|
+
|
|
90
107
|
const FILES = `**Files are named, not carried.** Where a tool takes attachments, give a path, an
|
|
91
108
|
HTTPS URL, or an attachment already on another message; the endpoint reads the
|
|
92
109
|
bytes. Never encode a file into a call — that is the thing this replaces.`;
|
|
@@ -122,6 +139,7 @@ const OWNER_HABITS: Record<string, string> = {
|
|
|
122
139
|
skills: SKILLS,
|
|
123
140
|
vault: VAULT,
|
|
124
141
|
setup: SETUP,
|
|
142
|
+
identity: IDENTITY,
|
|
125
143
|
};
|
|
126
144
|
|
|
127
145
|
/**
|
|
@@ -137,6 +155,15 @@ const OWNER_HABITS: Record<string, string> = {
|
|
|
137
155
|
* precisely the one that holds no skills directory, so the skill is not a place
|
|
138
156
|
* it can go. Only an endpoint serving remote clients spends it.
|
|
139
157
|
*
|
|
158
|
+
* Raised a second time, to 2500, for `IDENTITY`, and the same answer for the
|
|
159
|
+
* same reason: an agent signing as the wrong person has already sent the
|
|
160
|
+
* message, and a skill loaded only when relevant is not loaded at the moment
|
|
161
|
+
* that happens. The measured worst case — twenty profiles, twenty connections
|
|
162
|
+
* each, every owner provider reachable, remote clients — is 2474, so this is
|
|
163
|
+
* the measurement plus a little, not a round number picked first. Two things
|
|
164
|
+
* hold it there: the paragraph names no identity, and it is spent only by a
|
|
165
|
+
* profile that declared one.
|
|
166
|
+
*
|
|
140
167
|
* Exported because the test asserted `2000` as a literal while the code
|
|
141
168
|
* reserved room against a second, differently-derived number — so the two could
|
|
142
169
|
* disagree, and did. There is no separate listing allowance any more: `spent`
|
|
@@ -144,7 +171,7 @@ const OWNER_HABITS: Record<string, string> = {
|
|
|
144
171
|
* exactly the final length, because `join` adds the same two characters the
|
|
145
172
|
* reduce already counted.
|
|
146
173
|
*/
|
|
147
|
-
export const MAX_INSTRUCTIONS =
|
|
174
|
+
export const MAX_INSTRUCTIONS = 2500;
|
|
148
175
|
|
|
149
176
|
/** Which of the owner-layer providers this principal can actually reach. */
|
|
150
177
|
function ownerProviders(merged: ReadonlyMap<string, MergedCapability>): string[] {
|
|
@@ -20,6 +20,20 @@ import { allowedConnections } from '#policy';
|
|
|
20
20
|
* still a leak.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* A connection's reconciled state, as much of it as a reader needs.
|
|
25
|
+
*
|
|
26
|
+
* Structural rather than the store's own `ConnectionRecord`: `server` does not
|
|
27
|
+
* import `stores` (`src/architecture.test.ts`), and what a surface that reports
|
|
28
|
+
* wants from a connection is its key and whether it is working — not the
|
|
29
|
+
* timestamps and credential expiry the repository keeps behind it.
|
|
30
|
+
*/
|
|
31
|
+
export interface ConnectionState {
|
|
32
|
+
readonly provider: string;
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly status: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
23
37
|
/** Everything one profile contributes to the endpoint. */
|
|
24
38
|
export interface ProfileRuntime {
|
|
25
39
|
readonly config: Config;
|
|
@@ -35,6 +49,25 @@ export interface ProfileRuntime {
|
|
|
35
49
|
* how often to ask (ADR-014).
|
|
36
50
|
*/
|
|
37
51
|
refreshSkills?(): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Which target's adapters this profile was opened against.
|
|
54
|
+
*
|
|
55
|
+
* Not derivable from `config`: a target is *selected* per run, and the config
|
|
56
|
+
* only says which one is the default. Optional for the same reason the one
|
|
57
|
+
* below is — a runtime built to answer "what is visible" was never opened
|
|
58
|
+
* against anything.
|
|
59
|
+
*/
|
|
60
|
+
readonly target?: string | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Reconciled connection state, for a surface that reports rather than
|
|
63
|
+
* dispatches.
|
|
64
|
+
*
|
|
65
|
+
* Optional exactly as `refreshSkills` is: only a served endpoint holds the
|
|
66
|
+
* state handle this reads through, and nothing on the dispatch path asks —
|
|
67
|
+
* a capability's visibility is decided by policy, not by whether the
|
|
68
|
+
* credential behind it currently works.
|
|
69
|
+
*/
|
|
70
|
+
connections?(): Promise<readonly ConnectionState[]>;
|
|
38
71
|
}
|
|
39
72
|
|
|
40
73
|
export interface BuildServerOptions {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { containedKey, type BlobKey, type BlobMetadata, type BlobStore } from './index.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sending part of a store's key space somewhere else.
|
|
5
|
+
*
|
|
6
|
+
* `scopeBlobStore` narrows a store to a namespace; this one splits a store
|
|
7
|
+
* between backends. A key under a route's prefix is served by that route's
|
|
8
|
+
* store — which is rooted *at* the prefix, so the prefix is stripped on the way
|
|
9
|
+
* in and put back on the way out — and every other key falls through to the
|
|
10
|
+
* base.
|
|
11
|
+
*
|
|
12
|
+
* It exists for one shape and it is worth naming it, because the alternative
|
|
13
|
+
* was much larger. A profile's memory is not addressed by a name anything
|
|
14
|
+
* declares: core scopes the profile's blob root to `<provider>/<connection>`
|
|
15
|
+
* inside `buildProviderContext`, and `lanes link memory` reaches the same bytes
|
|
16
|
+
* by calling the same two functions. So "put memory somewhere else" is not a
|
|
17
|
+
* question the provider, the dispatcher, or the CLI can be asked — it is a
|
|
18
|
+
* property of the store all three were handed. Routing the root means none of
|
|
19
|
+
* them changes, and none of them can disagree about where an entry went.
|
|
20
|
+
*
|
|
21
|
+
* A route's store is expected to enforce its own containment: it is a
|
|
22
|
+
* `BlobStore` like any other, and the base's rules do not travel across the
|
|
23
|
+
* boundary.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export interface BlobRoute {
|
|
27
|
+
/**
|
|
28
|
+
* The key prefix this route claims, ending in `/`.
|
|
29
|
+
*
|
|
30
|
+
* Directory-shaped on purpose. A bare `memory` would also claim
|
|
31
|
+
* `memory-archive/x`, and a store that quietly swallows a neighbouring
|
|
32
|
+
* namespace is the kind of bug that surfaces as missing data rather than as
|
|
33
|
+
* an error.
|
|
34
|
+
*/
|
|
35
|
+
readonly prefix: string;
|
|
36
|
+
/** A store rooted at `prefix`, so it never sees the prefix itself. */
|
|
37
|
+
readonly store: BlobStore;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function routeBlobStore(base: BlobStore, routes: readonly BlobRoute[]): BlobStore {
|
|
41
|
+
if (routes.length === 0) return base;
|
|
42
|
+
|
|
43
|
+
const normalised = routes.map((route) => {
|
|
44
|
+
if (route.prefix.length === 0) {
|
|
45
|
+
throw new Error('A blob route prefix must not be empty — that is the base store.');
|
|
46
|
+
}
|
|
47
|
+
return { ...route, prefix: route.prefix.endsWith('/') ? route.prefix : `${route.prefix}/` };
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Which store owns this key, and what it calls it.
|
|
52
|
+
*
|
|
53
|
+
* Contained first, so `memory/../elsewhere.md` is judged by where it lands
|
|
54
|
+
* rather than by how it is spelled — routing on the raw string would let a
|
|
55
|
+
* traversal pick its own backend, which is a weaker answer than either store
|
|
56
|
+
* gives on its own.
|
|
57
|
+
*/
|
|
58
|
+
const routeFor = (key: BlobKey): { store: BlobStore; key: BlobKey } => {
|
|
59
|
+
const resolved = containedKey(key);
|
|
60
|
+
for (const route of normalised) {
|
|
61
|
+
if (resolved.startsWith(route.prefix)) {
|
|
62
|
+
return { store: route.store, key: resolved.slice(route.prefix.length) };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { store: base, key };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
async put(key, data, options) {
|
|
70
|
+
const to = routeFor(key);
|
|
71
|
+
return to.store.put(to.key, data, options);
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async get(key) {
|
|
75
|
+
const to = routeFor(key);
|
|
76
|
+
return to.store.get(to.key);
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
async has(key) {
|
|
80
|
+
const to = routeFor(key);
|
|
81
|
+
return to.store.has(to.key);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async delete(key) {
|
|
85
|
+
const to = routeFor(key);
|
|
86
|
+
return to.store.delete(to.key);
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Merge the listings, with routed keys removed from the base's.
|
|
91
|
+
*
|
|
92
|
+
* Three cases, and the middle one is the one that is easy to miss: a
|
|
93
|
+
* listing prefix *inside* a route belongs wholly to that route, a listing
|
|
94
|
+
* prefix *containing* a route has to include everything that route holds,
|
|
95
|
+
* and a listing that touches neither is the base's alone. Sorted at the end
|
|
96
|
+
* because callers rely on it — `skillFingerprint` sorts its own output, but
|
|
97
|
+
* `loadProfileSkills` reads the listing in order.
|
|
98
|
+
*/
|
|
99
|
+
async list(prefix) {
|
|
100
|
+
const asked = prefix ?? '';
|
|
101
|
+
|
|
102
|
+
for (const route of normalised) {
|
|
103
|
+
if (asked.startsWith(route.prefix)) {
|
|
104
|
+
const inner = await route.store.list(asked.slice(route.prefix.length));
|
|
105
|
+
return inner.map((entry) => ({ ...entry, key: `${route.prefix}${entry.key}` }));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const results: BlobMetadata[] = (await base.list(asked)).filter(
|
|
110
|
+
(entry) => !normalised.some((route) => entry.key.startsWith(route.prefix)),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
for (const route of normalised) {
|
|
114
|
+
if (!route.prefix.startsWith(asked)) continue;
|
|
115
|
+
for (const entry of await route.store.list()) {
|
|
116
|
+
results.push({ ...entry, key: `${route.prefix}${entry.key}` });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return results.sort((a, b) => a.key.localeCompare(b.key));
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|