@zackbart/connecta 0.37.1 → 0.38.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/CHANGELOG.md +31 -0
- package/README.md +2 -2
- package/bin/connecta.mjs +1 -39
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +2 -4
- package/dist/agent-identity.js +1 -1
- package/dist/auth/clerk-sdk/generated.js +2 -1
- package/dist/auth/downstream-oauth.js +19 -153
- package/dist/auth/downstream-revocation.js +1 -1
- package/dist/auth/oauth-grant.js +11 -0
- package/dist/auth/oauth-token-response.js +1 -1
- package/dist/auth/vercel-agent.js +2 -1
- package/dist/auth-elicitation.js +2 -2
- package/dist/branding.d.ts +1 -15
- package/dist/branding.js +2 -2
- package/dist/bytes.js +42 -0
- package/dist/call-admission-context.js +2 -5
- package/dist/call-recovery.js +0 -3
- package/dist/catalog-fingerprint.js +2 -18
- package/dist/catalog-service.js +1 -1
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +6 -2
- package/dist/config.d.ts +41 -14
- package/dist/config.js +21 -71
- package/dist/connectors/api-connector.js +4 -3
- package/dist/connectors/guarded-fetch.d.ts +0 -6
- package/dist/connectors/guarded-fetch.js +1 -11
- package/dist/connectors/remote-mcp.js +10 -43
- package/dist/connectors/resource-uri.js +0 -4
- package/dist/d1.d.ts +0 -89
- package/dist/d1.js +0 -34
- package/dist/downstream-input.js +2 -2
- package/dist/execute.js +22 -13
- package/dist/executor-result.js +2 -1
- package/dist/executors/quickjs-runtime.js +2 -25
- package/dist/executors/quickjs.js +1 -1
- package/dist/guest-runtime.js +5 -3
- package/dist/guest-types.d.ts +5 -5
- package/dist/http/client.js +1 -1
- package/dist/http/errors.js +2 -2
- package/dist/http/json-schema.js +1 -1
- package/dist/http/node-transport.js +0 -1
- package/dist/http/oauth.js +1 -1
- package/dist/http/paging.js +6 -9
- package/dist/http/provider.js +1 -1
- package/dist/http/safety.js +1 -1
- package/dist/http/services.js +1 -8
- package/dist/http/tools.js +1 -7
- package/dist/http/vendor.js +7 -7
- package/dist/identity.js +2 -6
- package/dist/invocation-auth.js +2 -1
- package/dist/meta-tools.js +27 -27
- package/dist/native-tools.js +2 -1
- package/dist/oauth-handoff.js +2 -2
- package/dist/operation-vocabulary.js +1 -1
- package/dist/operator-record.js +1 -49
- package/dist/operator-ui/contract.d.ts +2 -2
- package/dist/operator-ui/generated.js +3 -3
- package/dist/program-writes.js +3 -8
- package/dist/provider-call.js +0 -7
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/kit.js +1 -6
- package/dist/providers/_shared/google/service-account.js +1 -3
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/_shared/guide.js +3 -0
- package/dist/providers/_shared/json.js +12 -0
- package/dist/providers/_shared/rest/compact-index.js +1 -1
- package/dist/providers/_shared/rest/kit.js +1 -3
- package/dist/providers/_shared/rest/operation-index.js +1 -3
- package/dist/providers/_shared/rest/value-safety.js +26 -28
- package/dist/providers/breeze/api.js +2 -7
- package/dist/providers/breeze/index.js +4 -2
- package/dist/providers/breeze/tools.js +3 -4
- package/dist/providers/ccb/api.js +1 -6
- package/dist/providers/ccb/index.js +2 -1
- package/dist/providers/ccb/tools.js +1 -6
- package/dist/providers/cloudflare/api.js +2 -2
- package/dist/providers/cloudflare/index.js +2 -1
- package/dist/providers/cloudflare/safety.js +3 -3
- package/dist/providers/docs/tools.js +2 -10
- package/dist/providers/drive/tools.js +12 -25
- package/dist/providers/forms/tools.js +1 -9
- package/dist/providers/gmail/tools.js +5 -21
- package/dist/providers/overflow/api.js +1 -3
- package/dist/providers/overflow/index.js +2 -1
- package/dist/providers/overflow/tools.js +3 -7
- package/dist/providers/planning-center/api.js +2 -1
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/lib/json-api.js +1 -9
- package/dist/providers/planning-center/tools.js +2 -1
- package/dist/providers/sheets/tools.js +1 -9
- package/dist/providers/slides/tools.js +4 -13
- package/dist/providers/stripe/index.js +2 -1
- package/dist/providers/stripe/safety.js +2 -2
- package/dist/providers/tithely/api.js +2 -7
- package/dist/providers/tithely/index.js +3 -2
- package/dist/providers/tithely/tools.js +7 -5
- package/dist/providers/vercel/api.js +1 -6
- package/dist/providers/vercel/lib/pin.js +1 -1
- package/dist/providers/vercel/safety.js +1 -1
- package/dist/providers/vercel/tools.js +2 -1
- package/dist/redaction-marker.js +2 -2
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +5 -7
- package/dist/request-principal.js +1 -1
- package/dist/request-state.js +2 -2
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/activity-disclosure.js +1 -1
- package/dist/routes/connect.js +2 -2
- package/dist/routes/credentials.js +1 -1
- package/dist/routes/mcp.js +4 -3
- package/dist/routes/oauth-management.js +1 -1
- package/dist/routes/operator.js +4 -3
- package/dist/routes/shared.d.ts +8 -8
- package/dist/routes/shared.js +9 -9
- package/dist/routes/ui-config.js +1 -1
- package/dist/routes/ui.js +1 -1
- package/dist/runtime/call-admission.js +1 -1
- package/dist/runtime/call.js +1 -1
- package/dist/runtime/settlement.js +1 -1
- package/dist/runtime/shared-read.js +2 -15
- package/dist/sent-secrets.d.ts +1 -2
- package/dist/sent-secrets.js +3 -8
- package/dist/server.js +4 -4
- package/dist/skills.js +8 -35
- package/dist/sqlite.d.ts +0 -19
- package/dist/sqlite.js +2 -83
- package/dist/storage/keys.js +16 -125
- package/dist/storage/sql-native.js +0 -101
- package/dist/storage/sql.d.ts +1 -24
- package/dist/storage/sql.js +1 -12
- package/dist/tool-error.js +4 -0
- package/dist/url-safety.js +0 -4
- package/dist/usage-guide.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +1 -1
- package/documentation/README.md +6 -42
- package/documentation/architecture.md +86 -496
- package/documentation/auth.md +12 -25
- package/documentation/code-mode.md +90 -66
- package/documentation/deploying.md +19 -40
- package/documentation/integrating.md +4 -19
- package/documentation/meta-tools.md +6 -6
- package/documentation/migration-0.33.md +5 -5
- package/documentation/migration-0.34.md +2 -4
- package/documentation/migration-retired-layouts.md +74 -0
- package/documentation/operator-tests.md +19 -119
- package/documentation/operator-ui.md +3 -4
- package/documentation/provider-auth-migration.md +4 -10
- package/documentation/provider-migration-0.29.md +4 -15
- package/examples/worker/AGENTS.md +4 -6
- package/examples/worker/README.md +20 -115
- package/guides/connecting-agents.md +7 -30
- package/guides/how-it-works.md +2 -2
- package/guides/safety-and-trust.md +4 -1
- package/package.json +19 -19
- package/templates/node/README.md +9 -26
- package/templates/node/package.json +1 -1
- package/dist/auth/oauth-v2-migration.js +0 -135
- package/dist/provider-call-facts.d.ts +0 -115
- package/dist/provider-call.d.ts +0 -112
- package/dist/storage/d1-copy.js +0 -255
- package/dist/storage/sql-schema.d.ts +0 -6
- package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
- package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
|
@@ -158,7 +158,7 @@ function credentialRequest(context, connectorId, action) {
|
|
|
158
158
|
if (request.method === "DELETE") {
|
|
159
159
|
// Clearing nothing is refused, as testing nothing is, so the route
|
|
160
160
|
// agrees with the operator contract, which offers `clear` only once a
|
|
161
|
-
// value is stored in the caller's partition
|
|
161
|
+
// value is stored in the caller's partition. A value that cannot
|
|
162
162
|
// be read may still be cleared: removing it is how one recovers.
|
|
163
163
|
const stored = yield* Effect.tryPromise({
|
|
164
164
|
try: () => vault.metadata(connectorId, owner),
|
package/dist/routes/mcp.js
CHANGED
|
@@ -32,7 +32,7 @@ const deadlineResponses = new WeakSet();
|
|
|
32
32
|
/**
|
|
33
33
|
* The most a `subscriptions/listen` classification reads. A listen is a small
|
|
34
34
|
* JSON-RPC request; this read happens before request admission, so it is
|
|
35
|
-
* bounded by this fixed cap rather than by the host's body limit
|
|
35
|
+
* bounded by this fixed cap rather than by the host's body limit.
|
|
36
36
|
*/
|
|
37
37
|
export const MAX_LISTEN_CLASSIFICATION_BYTES = 64 * 1024;
|
|
38
38
|
/**
|
|
@@ -286,7 +286,7 @@ function admitted(controller, signal) {
|
|
|
286
286
|
}), (lease) => Effect.sync(() => lease.release()), { interruptible: true });
|
|
287
287
|
}
|
|
288
288
|
/**
|
|
289
|
-
* 404 for any `?toolkit=` value, kept after the feature's retirement
|
|
289
|
+
* 404 for any `?toolkit=` value, kept after the feature's retirement.
|
|
290
290
|
*
|
|
291
291
|
* Toolkits are gone, but the URLs that named them are not: clients were handed
|
|
292
292
|
* MCP endpoint URLs with `?toolkit=` baked in, and nothing about upgrading the
|
|
@@ -409,6 +409,7 @@ function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, re
|
|
|
409
409
|
credentialHandoffUrl: opts.config.ui?.credentialHandoffUrl(baseUrl),
|
|
410
410
|
...(activity ? { activity } : {}),
|
|
411
411
|
defaultToolTimeoutMs: opts.config.calls.defaultTimeoutMs ?? opts.config.execute.hostCallTimeoutMs,
|
|
412
|
+
maxProgramResultChars: opts.config.execute.maxResultChars,
|
|
412
413
|
probeTimeoutMs: opts.config.discovery.probeTimeoutMs,
|
|
413
414
|
discoveryConcurrency: opts.config.discovery.concurrency,
|
|
414
415
|
requestSignal,
|
|
@@ -638,7 +639,7 @@ export function createMcpRoute(opts) {
|
|
|
638
639
|
}));
|
|
639
640
|
// A request naming `subscriptions/listen` defers its permit: nothing of
|
|
640
641
|
// its body is read until auth and endpoint admission pass, and then at
|
|
641
|
-
// most MAX_LISTEN_CLASSIFICATION_BYTES
|
|
642
|
+
// most MAX_LISTEN_CLASSIFICATION_BYTES. Only a body-confirmed
|
|
642
643
|
// modern listen goes without a permit; trusting the header alone would
|
|
643
644
|
// let a legacy tools/call bypass the pool, so anything else takes one
|
|
644
645
|
// after classification. Every other request takes its permit here,
|
|
@@ -23,7 +23,7 @@ function oauthManagementRequest(context, connectorId) {
|
|
|
23
23
|
const registry = yield* managementRegistry(context, authz);
|
|
24
24
|
const connector = registry.getConnector(connectorId);
|
|
25
25
|
const disconnectAuth = connector?.disconnectAuth?.bind(connector);
|
|
26
|
-
// Each verb needs only its own hooks
|
|
26
|
+
// Each verb needs only its own hooks: connecting needs the whole
|
|
27
27
|
// flow, through the callback's state check; disconnecting needs only
|
|
28
28
|
// disconnectAuth, so a grant provisioned out of band can still be dropped.
|
|
29
29
|
const connects = Boolean(connector?.startAuth && connector.finishAuth && connector.verifyState);
|
package/dist/routes/operator.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bytesToHex } from "../bytes.js";
|
|
1
2
|
// The Effect plumbing the operator and activity data routes share.
|
|
2
3
|
//
|
|
3
4
|
// Each JSON route under /ui/* is one Effect program, run at its Promise edge
|
|
@@ -84,7 +85,7 @@ export function operatorViewerKey(context, authz) {
|
|
|
84
85
|
]));
|
|
85
86
|
const signingKey = key;
|
|
86
87
|
const digest = yield* attempt(() => crypto.subtle.sign("HMAC", signingKey, identity));
|
|
87
|
-
return
|
|
88
|
+
return bytesToHex(new Uint8Array(digest));
|
|
88
89
|
}), {
|
|
89
90
|
timeoutMs: 500,
|
|
90
91
|
signal: context.request.signal,
|
|
@@ -101,7 +102,7 @@ export function scopeFor(authz, authenticated, connectorIds = "all") {
|
|
|
101
102
|
};
|
|
102
103
|
}
|
|
103
104
|
/**
|
|
104
|
-
* The whole registry, for a maintainer only
|
|
105
|
+
* The whole registry, for a maintainer only. Everyone else gets the
|
|
105
106
|
* private 404 plain `/mcp` gives them.
|
|
106
107
|
*/
|
|
107
108
|
export function visibleRegistry({ opts }, authz) {
|
|
@@ -111,7 +112,7 @@ export function visibleRegistry({ opts }, authz) {
|
|
|
111
112
|
return Effect.succeed(opts.registry.scoped(scopeFor(authz, opts.config.auth.length > 0)));
|
|
112
113
|
}
|
|
113
114
|
/**
|
|
114
|
-
* The connectors a connector-auth route may resolve
|
|
115
|
+
* The connectors a connector-auth route may resolve: every connector
|
|
115
116
|
* for a maintainer, the connectors of their pools for a pool member, and the
|
|
116
117
|
* private 404 for anyone else. mayManageConnector still decides each action.
|
|
117
118
|
*/
|
package/dist/routes/shared.d.ts
CHANGED
|
@@ -78,7 +78,7 @@ export declare function authorize(request: Request, baseUrl: string, auth: reado
|
|
|
78
78
|
response: Response;
|
|
79
79
|
}>;
|
|
80
80
|
/**
|
|
81
|
-
* Admission at an agent endpoint, `/agent/<pool
|
|
81
|
+
* Admission at an agent endpoint, `/agent/<pool>`. Only the built-in
|
|
82
82
|
* agent credential adapters are consulted: Clerk and Access never run, cookies and Clerk
|
|
83
83
|
* handshake parameters are stripped, and the runtime context, which carries
|
|
84
84
|
* Access's `ctx.access` even on a bypassed path, is never passed. Anything but
|
|
@@ -88,31 +88,31 @@ export declare function authorizeAgent(request: Request, baseUrl: string, auth:
|
|
|
88
88
|
/** Human routes reject machine credentials before any verification or storage I/O. */
|
|
89
89
|
export declare function authorizeUiIdentity(request: Request, baseUrl: string, auth: readonly InboundAuth[], purpose: string, runtimeContext?: RuntimeExecutionContext, logger?: Logger): Promise<Awaited<ReturnType<typeof authorize>>>;
|
|
90
90
|
export declare function isSameOrigin(request: Request, baseUrl: string): boolean;
|
|
91
|
-
export declare function withSecurityHeaders(response: Response, requestUrl: URL
|
|
91
|
+
export declare function withSecurityHeaders(response: Response, requestUrl: URL): Response;
|
|
92
92
|
type AuthorizedIdentity = Extract<Awaited<ReturnType<typeof authorize>>, {
|
|
93
93
|
ok: true;
|
|
94
94
|
}>;
|
|
95
95
|
/**
|
|
96
|
-
* Plain `/mcp` and every operator route admit a maintainer
|
|
96
|
+
* Plain `/mcp` and every operator route admit a maintainer. An open
|
|
97
97
|
* deployment has no inbound auth and so no maintainers: its one anonymous
|
|
98
98
|
* caller keeps the whole default endpoint, as the startup warning says.
|
|
99
99
|
*/
|
|
100
100
|
export declare function admitsMaintainer(opts: ServerOptions, authz: AuthorizedIdentity): boolean;
|
|
101
101
|
/**
|
|
102
102
|
* The declared pool `/mcp/<name>` serves to this person, or undefined for the
|
|
103
|
-
* same 404. An agent is never admitted here, even when listed
|
|
103
|
+
* same 404. An agent is never admitted here, even when listed.
|
|
104
104
|
*/
|
|
105
105
|
export declare function memberPool(opts: ServerOptions, name: string, identity: Readonly<AuthenticatedIdentity>): ResolvedPool | undefined;
|
|
106
106
|
/** The declared pool `/agent/<name>` serves to this agent, or undefined for the same 404. */
|
|
107
107
|
export declare function agentPool(opts: ServerOptions, name: string, identity: Readonly<AuthenticatedIdentity>): ResolvedPool | undefined;
|
|
108
108
|
/**
|
|
109
|
-
* The connectors of every pool this identity is a member of
|
|
109
|
+
* The connectors of every pool this identity is a member of: what a
|
|
110
110
|
* pool member may ever manage, and only their personal ones among these.
|
|
111
111
|
*/
|
|
112
112
|
export declare function memberConnectors(opts: ServerOptions, identity: Readonly<AuthenticatedIdentity>): "all" | ReadonlySet<string>;
|
|
113
113
|
/**
|
|
114
114
|
* Whether this identity may start, finish, or remove a connector's auth
|
|
115
|
-
*
|
|
115
|
+
* Only an interactive person ever may; an agent or any other machine
|
|
116
116
|
* credential manages nothing. Shared auth is deployment-wide, so it takes a
|
|
117
117
|
* maintainer. A personal connector takes a stable principal that is a
|
|
118
118
|
* maintainer, whose plain `/mcp` includes it, or a member of a pool that
|
|
@@ -124,10 +124,10 @@ export declare function mayManageConnector(opts: ServerOptions, authz: Authorize
|
|
|
124
124
|
id: string;
|
|
125
125
|
authScope?: "shared" | "personal";
|
|
126
126
|
}): boolean;
|
|
127
|
-
/** Agent token list, issue, rotate, and revoke: an interactive maintainer only
|
|
127
|
+
/** Agent token list, issue, rotate, and revoke: an interactive maintainer only. */
|
|
128
128
|
export declare function managesAgentTokens(opts: ServerOptions, authz: AuthorizedIdentity): boolean;
|
|
129
129
|
/**
|
|
130
|
-
* Activity reads: a maintainer principal, interactive or not
|
|
130
|
+
* Activity reads: a maintainer principal, interactive or not. Members
|
|
131
131
|
* and agents read none, and an open deployment has no maintainer.
|
|
132
132
|
*/
|
|
133
133
|
export declare function readsActivity(opts: ServerOptions, authz: AuthorizedIdentity): boolean;
|
package/dist/routes/shared.js
CHANGED
|
@@ -163,7 +163,7 @@ const authorizeNative = /* @__PURE__ */ registerAuth(authorize, function (reques
|
|
|
163
163
|
return { ok: false, response: privateJson({ error: "authenticated user required" }, { status: 403 }) };
|
|
164
164
|
}
|
|
165
165
|
// Every caller of this function is a human route. Agents reach pools only at
|
|
166
|
-
// `/agent/<pool
|
|
166
|
+
// `/agent/<pool>`, so a workload or `cta_` credential refuses here
|
|
167
167
|
// without verification, and never falls back to a cookie, session or Access context.
|
|
168
168
|
if (machineCredential) {
|
|
169
169
|
return {
|
|
@@ -299,7 +299,7 @@ function agentChallenge() {
|
|
|
299
299
|
};
|
|
300
300
|
}
|
|
301
301
|
/**
|
|
302
|
-
* Admission at an agent endpoint, `/agent/<pool
|
|
302
|
+
* Admission at an agent endpoint, `/agent/<pool>`. Only the built-in
|
|
303
303
|
* agent credential adapters are consulted: Clerk and Access never run, cookies and Clerk
|
|
304
304
|
* handshake parameters are stripped, and the runtime context, which carries
|
|
305
305
|
* Access's `ctx.access` even on a bypassed path, is never passed. Anything but
|
|
@@ -376,7 +376,7 @@ export function isSameOrigin(request, baseUrl) {
|
|
|
376
376
|
return false;
|
|
377
377
|
}
|
|
378
378
|
}
|
|
379
|
-
export function withSecurityHeaders(response, requestUrl
|
|
379
|
+
export function withSecurityHeaders(response, requestUrl) {
|
|
380
380
|
const headers = response.headers.get("Content-Type")?.toLowerCase().startsWith("text/html")
|
|
381
381
|
? htmlSecurityHeaders(response.headers)
|
|
382
382
|
: new Headers(response.headers);
|
|
@@ -392,7 +392,7 @@ export function withSecurityHeaders(response, requestUrl, _path) {
|
|
|
392
392
|
});
|
|
393
393
|
}
|
|
394
394
|
/**
|
|
395
|
-
* Plain `/mcp` and every operator route admit a maintainer
|
|
395
|
+
* Plain `/mcp` and every operator route admit a maintainer. An open
|
|
396
396
|
* deployment has no inbound auth and so no maintainers: its one anonymous
|
|
397
397
|
* caller keeps the whole default endpoint, as the startup warning says.
|
|
398
398
|
*/
|
|
@@ -401,7 +401,7 @@ export function admitsMaintainer(opts, authz) {
|
|
|
401
401
|
}
|
|
402
402
|
/**
|
|
403
403
|
* The declared pool `/mcp/<name>` serves to this person, or undefined for the
|
|
404
|
-
* same 404. An agent is never admitted here, even when listed
|
|
404
|
+
* same 404. An agent is never admitted here, even when listed.
|
|
405
405
|
*/
|
|
406
406
|
export function memberPool(opts, name, identity) {
|
|
407
407
|
if (identity.agent !== undefined)
|
|
@@ -417,7 +417,7 @@ export function agentPool(opts, name, identity) {
|
|
|
417
417
|
return pool?.members.some((member) => member.kind === "agent" && isMember(member, identity)) ? pool : undefined;
|
|
418
418
|
}
|
|
419
419
|
/**
|
|
420
|
-
* The connectors of every pool this identity is a member of
|
|
420
|
+
* The connectors of every pool this identity is a member of: what a
|
|
421
421
|
* pool member may ever manage, and only their personal ones among these.
|
|
422
422
|
*/
|
|
423
423
|
export function memberConnectors(opts, identity) {
|
|
@@ -435,7 +435,7 @@ export function memberConnectors(opts, identity) {
|
|
|
435
435
|
}
|
|
436
436
|
/**
|
|
437
437
|
* Whether this identity may start, finish, or remove a connector's auth
|
|
438
|
-
*
|
|
438
|
+
* Only an interactive person ever may; an agent or any other machine
|
|
439
439
|
* credential manages nothing. Shared auth is deployment-wide, so it takes a
|
|
440
440
|
* maintainer. A personal connector takes a stable principal that is a
|
|
441
441
|
* maintainer, whose plain `/mcp` includes it, or a member of a pool that
|
|
@@ -454,12 +454,12 @@ export function mayManageConnector(opts, authz, connector) {
|
|
|
454
454
|
const pools = memberConnectors(opts, identity);
|
|
455
455
|
return pools === "all" || pools.has(connector.id);
|
|
456
456
|
}
|
|
457
|
-
/** Agent token list, issue, rotate, and revoke: an interactive maintainer only
|
|
457
|
+
/** Agent token list, issue, rotate, and revoke: an interactive maintainer only. */
|
|
458
458
|
export function managesAgentTokens(opts, authz) {
|
|
459
459
|
return (authz.identity.interactive && authz.identity.agent === undefined && isMaintainer(opts.maintainers, authz.identity));
|
|
460
460
|
}
|
|
461
461
|
/**
|
|
462
|
-
* Activity reads: a maintainer principal, interactive or not
|
|
462
|
+
* Activity reads: a maintainer principal, interactive or not. Members
|
|
463
463
|
* and agents read none, and an open deployment has no maintainer.
|
|
464
464
|
*/
|
|
465
465
|
export function readsActivity(opts, authz) {
|
package/dist/routes/ui-config.js
CHANGED
|
@@ -359,7 +359,7 @@ function maintainerRead(context, authz) {
|
|
|
359
359
|
.map(([id, tools]) => [id, Object.fromEntries(Object.entries(tools).filter(([name]) => validName(name)))]));
|
|
360
360
|
// A pool owner is a Workspace mailbox address. Only an authenticated
|
|
361
361
|
// maintainer reads it; an open deployment's anonymous caller, which this
|
|
362
|
-
// route admits, gets null, the same as an unset owner
|
|
362
|
+
// route admits, gets null, the same as an unset owner.
|
|
363
363
|
const ownersVisible = opts.config.auth.length > 0 && isMaintainer(opts.maintainers, authz.identity);
|
|
364
364
|
config.pools = config.pools.map((pool) => ({
|
|
365
365
|
...pool,
|
package/dist/routes/ui.js
CHANGED
|
@@ -182,7 +182,7 @@ function summary(context) {
|
|
|
182
182
|
const authz = yield* authorized(context);
|
|
183
183
|
const registry = yield* visibleRegistry(context, authz);
|
|
184
184
|
const { visible, mayManage, permissions, activityEnabled, credentialManagement } = operatorView(context, authz, registry);
|
|
185
|
-
// Only maintainers reach here
|
|
185
|
+
// Only maintainers reach here; setup commands are offered for every
|
|
186
186
|
// declared pool, since they administer them all.
|
|
187
187
|
const pools = [...opts.pools.keys()];
|
|
188
188
|
return privateJson({
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// block: the one place outside its instance methods that may read its private
|
|
8
8
|
// bookkeeping, and one that emits nothing into the `.d.ts`.
|
|
9
9
|
//
|
|
10
|
-
// The two controllers stay separate on purpose
|
|
10
|
+
// The two controllers stay separate on purpose: request admission
|
|
11
11
|
// bounds executor work, this bounds calls to one downstream connector, and a
|
|
12
12
|
// shared queue between them was refused.
|
|
13
13
|
import { Effect } from "effect";
|
package/dist/runtime/call.js
CHANGED
|
@@ -106,7 +106,7 @@ function ended() {
|
|
|
106
106
|
return new ConnectorCallError("connector_call_failed", "This call has ended; a capability kept from it can no longer be used.", { retryable: false });
|
|
107
107
|
}
|
|
108
108
|
/** Build one call's services. Invocation calls this in the scoped attempt that owns the permit (B.1). */
|
|
109
|
-
|
|
109
|
+
function openCall(inputs) {
|
|
110
110
|
let open = true;
|
|
111
111
|
const parentSignal = inputs.parent ? Context.get(inputs.parent.services, Signal) : undefined;
|
|
112
112
|
const parents = [inputs.signal, parentSignal, inputs.requestSignal].filter((signal) => signal !== undefined);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// it was created.") when the Worker's compatibility date is 2025-06-16 or
|
|
11
11
|
// later, in releases since September 2026. The refusal throws out of the call
|
|
12
12
|
// that completed the Deferred, failing the request that released a slot, and
|
|
13
|
-
// the waiter never resumes
|
|
13
|
+
// the waiter never resumes. Awaiting a Promise around that wait is no
|
|
14
14
|
// help: the fiber waiting on the Deferred inside the Promise is resumed in the
|
|
15
15
|
// completing request just the same.
|
|
16
16
|
//
|
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// Discovery and the registry both coalesce a connector's catalog read inside
|
|
4
|
-
// one request, so a search, a describe, and the call after them cost one
|
|
5
|
-
// downstream listing. The read used to run under whichever reader asked
|
|
6
|
-
// first: its signal was the read's signal, so a short-deadline call that
|
|
7
|
-
// happened to start the read failed a search that joined it with the call's
|
|
8
|
-
// timeout, and a cancelled search cancelled a read others were still waiting
|
|
9
|
-
// on (#571).
|
|
10
|
-
//
|
|
11
|
-
// So the read owns its signal. Each reader waits under its own cancellation,
|
|
12
|
-
// and the read is cancelled only when no reader is left to want it. Nothing
|
|
13
|
-
// here is shared across requests: a SharedRead lives in one request's map and
|
|
14
|
-
// is waited on only by that request's fibers, which is why a plain count of
|
|
15
|
-
// readers is enough and no edge is needed between them.
|
|
1
|
+
// Request-local catalog reads own their signal. Each reader waits under its own
|
|
2
|
+
// cancellation; the read is cancelled only when no reader remains (INV-7).
|
|
16
3
|
import { Deferred, Effect, Exit } from "effect";
|
|
17
4
|
import { forkRequest } from "./run.js";
|
|
18
5
|
import { requestOwner } from "./request-scope.js";
|
package/dist/sent-secrets.d.ts
CHANGED
|
@@ -224,7 +224,6 @@ export declare function agentOutputOperations<T extends Record<string, (...args:
|
|
|
224
224
|
/** Slot reads cover custom handlers too, including keys put in query strings. A field named
|
|
225
225
|
* as an identifier (an application or client ID, a username, a public key) is matched whole. */
|
|
226
226
|
export declare function registerCredentialValues(secrets: SentSecrets, values: import("./types.js").ConnectorCredentialValues | null): void;
|
|
227
|
-
export declare function trackCredentialReads(ctx: ConnectorContext): void;
|
|
228
227
|
/** A connector's result or error as it enters: every connector's credentials and the
|
|
229
228
|
* request's private arguments, under payload rules. */
|
|
230
229
|
export declare function redactSentSecrets<T>(ctx: ConnectorContext, value: T): T;
|
|
@@ -235,7 +234,7 @@ export declare function requestSentSecrets(ctx: ConnectorContext): SentSecrets;
|
|
|
235
234
|
* extracted from. Redact it whole first: a cut through an echoed form can leave a remnant
|
|
236
235
|
* shorter than any fragment the output boundary matches, and a normalized number or
|
|
237
236
|
* extracted substring may no longer hold the form at all. */
|
|
238
|
-
export declare
|
|
237
|
+
export declare const redactBeforeCut: typeof redactSentSecrets;
|
|
239
238
|
/** Base64, or base64url with `url`, of bytes or UTF-8 text Connecta returns to an agent: a
|
|
240
239
|
* download, an attachment, a REST body, a page cursor. Every such encoding goes through here,
|
|
241
240
|
* since no later pass reads through an encoding the output does not declare: the content is
|
package/dist/sent-secrets.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Effect } from "effect";
|
|
|
5
5
|
import { classificationCode, ConnectorCallError } from "./errors.js";
|
|
6
6
|
import { credentialUrlViews } from "./credential-url.js";
|
|
7
7
|
import { RequestScope, requestOwner } from "./runtime/request-scope.js";
|
|
8
|
-
import { viewSecrets
|
|
8
|
+
import { viewSecrets } from "./runtime/call.js";
|
|
9
9
|
import { visitPrivateCallArguments } from "./argument-redaction.js";
|
|
10
10
|
import { keptErrorName, redactedCopyOf } from "./redaction-identity.js";
|
|
11
11
|
import { REDACTED } from "./redaction-marker.js";
|
|
@@ -688,7 +688,7 @@ function headerSpace(text, at) {
|
|
|
688
688
|
}
|
|
689
689
|
/**
|
|
690
690
|
* The echo's spans with the value of every sensitive header line (`headerLine`'s names) added,
|
|
691
|
-
* read on the text before the echo is replaced
|
|
691
|
+
* read on the text before the echo is replaced. An echoed private value can take part of a
|
|
692
692
|
* header's name, its `:`, or a credential prefix such as `Bearer `, so the line cannot be read
|
|
693
693
|
* after replacement; read here, the whole value goes, every cookie pair and any short tail with
|
|
694
694
|
* it. A line that is not a sensitive header keeps its text. One forward pass; nothing is kept.
|
|
@@ -2512,9 +2512,6 @@ export function registerCredentialValues(secrets, values) {
|
|
|
2512
2512
|
secrets.add(value);
|
|
2513
2513
|
}
|
|
2514
2514
|
}
|
|
2515
|
-
export function trackCredentialReads(ctx) {
|
|
2516
|
-
wrapViewCredentials(ctx);
|
|
2517
|
-
}
|
|
2518
2515
|
/** A connector's result or error as it enters: every connector's credentials and the
|
|
2519
2516
|
* request's private arguments, under payload rules. */
|
|
2520
2517
|
export function redactSentSecrets(ctx, value) {
|
|
@@ -2530,9 +2527,7 @@ export function requestSentSecrets(ctx) {
|
|
|
2530
2527
|
* extracted from. Redact it whole first: a cut through an echoed form can leave a remnant
|
|
2531
2528
|
* shorter than any fragment the output boundary matches, and a normalized number or
|
|
2532
2529
|
* extracted substring may no longer hold the form at all. */
|
|
2533
|
-
export
|
|
2534
|
-
return requestSentSecrets(ctx).redact(value);
|
|
2535
|
-
}
|
|
2530
|
+
export const redactBeforeCut = redactSentSecrets;
|
|
2536
2531
|
/** Base64, or base64url with `url`, of bytes or UTF-8 text Connecta returns to an agent: a
|
|
2537
2532
|
* download, an attachment, a REST body, a page cursor. Every such encoding goes through here,
|
|
2538
2533
|
* since no later pass reads through an encoding the output does not declare: the content is
|
package/dist/server.js
CHANGED
|
@@ -35,7 +35,7 @@ export function createFetchHandler(opts) {
|
|
|
35
35
|
// Which sandbox, not how it is tuned: `connecta doctor` reports the
|
|
36
36
|
// executor it just exercised rather than assuming one, and a
|
|
37
37
|
// deployment whose executor identifies as nothing omits the key
|
|
38
|
-
// instead of inviting a guess
|
|
38
|
+
// instead of inviting a guess.
|
|
39
39
|
...(opts.executorName !== undefined ? { executor: { name: opts.executorName } } : {}),
|
|
40
40
|
admission: {
|
|
41
41
|
policy: "global-fifo",
|
|
@@ -64,7 +64,7 @@ export function createFetchHandler(opts) {
|
|
|
64
64
|
const defer = runtimeContext ? runtimeContext.waitUntil.bind(runtimeContext) : undefined;
|
|
65
65
|
const originRefusal = routeMcp.rejectOrigin(request);
|
|
66
66
|
if (originRefusal) {
|
|
67
|
-
return Effect.succeed(withSecurityHeaders(originRefusal, url
|
|
67
|
+
return Effect.succeed(withSecurityHeaders(originRefusal, url));
|
|
68
68
|
}
|
|
69
69
|
// Container and orchestrator probes reach /health over plain HTTP on
|
|
70
70
|
// loopback, where no proxy has set X-Forwarded-Proto. Redirecting them to
|
|
@@ -87,7 +87,7 @@ export function createFetchHandler(opts) {
|
|
|
87
87
|
return Effect.succeed(withSecurityHeaders(new Response(null, {
|
|
88
88
|
status: 308,
|
|
89
89
|
headers: { Location: target.toString() },
|
|
90
|
-
}), url
|
|
90
|
+
}), url));
|
|
91
91
|
}
|
|
92
92
|
const context = {
|
|
93
93
|
request,
|
|
@@ -98,7 +98,7 @@ export function createFetchHandler(opts) {
|
|
|
98
98
|
defer,
|
|
99
99
|
runtimeContext,
|
|
100
100
|
};
|
|
101
|
-
return Effect.map(route(context), (response) => withSecurityHeaders(response, url
|
|
101
|
+
return Effect.map(route(context), (response) => withSecurityHeaders(response, url));
|
|
102
102
|
});
|
|
103
103
|
return function fetch(request, runtimeContext) {
|
|
104
104
|
return runEdge(serve(request, runtimeContext), { signal: request.signal });
|
package/dist/skills.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bytesToHex, utf8Length } from "./bytes.js";
|
|
1
2
|
import { duringViewEffect } from "./runtime/call.js";
|
|
2
3
|
import { RequestScope, requestOwner } from "./runtime/request-scope.js";
|
|
3
4
|
import { upwardStep } from "./upward.js";
|
|
@@ -9,7 +10,7 @@ import { runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
|
9
10
|
import { closeScopeOnExit } from "./runtime/connector-scope.js";
|
|
10
11
|
import { sentSecretsForRequest } from "./sent-secrets.js";
|
|
11
12
|
const ROUTE = "Choose a route before discovery. One known-address read uses call_tool; one known-address write uses call_destructive_tool. Unknown-address read-only work starts with execute_code to discover, call, and return the answer; use the same route for reduction, multiple or dependent calls, loops, joins, or branches. Keep discovery and calls together when schemas suffice; do not return catalog matches alone. Sample unfamiliar reads.";
|
|
12
|
-
const RECOVERY = 'When an auth error includes a handoff, give it to the user or operator. Use authorize_connector for a fresh link. Page resultId notices and format:"paged" handles with connecta.result(handleOrId, { page? }).
|
|
13
|
+
const RECOVERY = 'When an auth error includes a handoff, give it to the user or operator. Use authorize_connector for a fresh link. Page resultId notices and format:"paged" handles with connecta.result(handleOrId, { page? }). Missing guidance/repair only: skills tool "usage" or connecta.skill("usage").';
|
|
13
14
|
/**
|
|
14
15
|
* The always-loaded MCP `instructions` string. An endpoint is writable or
|
|
15
16
|
* read-only: a writable one lets programs and call_destructive_tool write,
|
|
@@ -328,25 +329,6 @@ const MAX_SKILL_CATALOG_BYTES = 8 * 1024 * 1024;
|
|
|
328
329
|
const MAX_SKILLS = 1_024;
|
|
329
330
|
const encoder = new TextEncoder();
|
|
330
331
|
const PRIVATE = { resultType: "complete", ttlMs: 0, cacheScope: "private" };
|
|
331
|
-
/** Bound metadata before cloning it, without another payload-sized byte array. */
|
|
332
|
-
function jsonBytes(value) {
|
|
333
|
-
const json = JSON.stringify(value);
|
|
334
|
-
let bytes = 0;
|
|
335
|
-
for (let i = 0; i < json.length; i++) {
|
|
336
|
-
const code = json.charCodeAt(i);
|
|
337
|
-
if (code < 0x80)
|
|
338
|
-
bytes++;
|
|
339
|
-
else if (code < 0x800)
|
|
340
|
-
bytes += 2;
|
|
341
|
-
else if (code >= 0xd800 && code <= 0xdbff && json.charCodeAt(i + 1) >= 0xdc00 && json.charCodeAt(i + 1) <= 0xdfff) {
|
|
342
|
-
bytes += 4;
|
|
343
|
-
i++;
|
|
344
|
-
}
|
|
345
|
-
else
|
|
346
|
-
bytes += 3;
|
|
347
|
-
}
|
|
348
|
-
return bytes;
|
|
349
|
-
}
|
|
350
332
|
function localRecord(name, uri, description, content, aliases) {
|
|
351
333
|
return { entry: { uri, frontmatter: { name, description }, resources: "dynamic" }, content, aliases };
|
|
352
334
|
}
|
|
@@ -369,12 +351,7 @@ const localRecordsNative = /* @__PURE__ */ registerAuth(localRecords, function (
|
|
|
369
351
|
(a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), (connector) => authFlow(function* () {
|
|
370
352
|
const name = validSkillName(connector.id)
|
|
371
353
|
? connector.id
|
|
372
|
-
: `connector-${[
|
|
373
|
-
...new Uint8Array(yield* upwardStep(authInvoke(crypto.subtle, "digest", ["SHA-256", encoder.encode(connector.id)]))),
|
|
374
|
-
]
|
|
375
|
-
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
376
|
-
.join("")
|
|
377
|
-
.slice(0, 48)}`;
|
|
354
|
+
: `connector-${bytesToHex(new Uint8Array(yield* upwardStep(authInvoke(crypto.subtle, "digest", ["SHA-256", encoder.encode(connector.id)])))).slice(0, 48)}`;
|
|
378
355
|
const description = connectorGuideSummary(connector);
|
|
379
356
|
const root = `skill://connecta/connectors/${encodeURIComponent(name)}`;
|
|
380
357
|
const content = `---\n${JSON.stringify({ name, description }, null, 2)}\n---\n\n${connectorGuide(connector)}`;
|
|
@@ -474,11 +451,7 @@ const withManifestNative = /* @__PURE__ */ registerAuth(withManifest, function (
|
|
|
474
451
|
const bytes = encoder.encode(record.content);
|
|
475
452
|
if (bytes.length > MAX_SKILL_BYTES)
|
|
476
453
|
throw new ConnectorCallError("unavailable", "Local skill exceeds the byte bound.");
|
|
477
|
-
const digest = [
|
|
478
|
-
...new Uint8Array(yield* upwardStep(authInvoke(crypto.subtle, "digest", ["SHA-256", bytes]))),
|
|
479
|
-
]
|
|
480
|
-
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
481
|
-
.join("");
|
|
454
|
+
const digest = bytesToHex(new Uint8Array(yield* upwardStep(authInvoke(crypto.subtle, "digest", ["SHA-256", bytes]))));
|
|
482
455
|
return {
|
|
483
456
|
...record,
|
|
484
457
|
entry: {
|
|
@@ -723,7 +696,7 @@ export class SkillsRegistry {
|
|
|
723
696
|
const records = yield* upwardStep(Effect.forEach(yield* upwardStep(self.local), (record) => authCall(withManifest, [record]), {
|
|
724
697
|
concurrency: "unbounded",
|
|
725
698
|
}));
|
|
726
|
-
let catalogBytes =
|
|
699
|
+
let catalogBytes = utf8Length(JSON.stringify(records.map((record) => record.entry)));
|
|
727
700
|
// Bounded sequential connector reads avoid unbounded fan-out and finish
|
|
728
701
|
// cleanup before a later connector's failure can end the whole listing.
|
|
729
702
|
for (const connector of self.registry.listConnectors()) {
|
|
@@ -736,7 +709,7 @@ export class SkillsRegistry {
|
|
|
736
709
|
if (!Array.isArray(entries) || records.length + entries.length > MAX_SKILLS)
|
|
737
710
|
throw new ConnectorCallError("unavailable", "Downstream skills listing exceeds its bound.");
|
|
738
711
|
for (const original of entries) {
|
|
739
|
-
if (catalogBytes +
|
|
712
|
+
if (catalogBytes + utf8Length(JSON.stringify(original)) + 1 > MAX_SKILL_CATALOG_BYTES)
|
|
740
713
|
throw new ConnectorCallError("unavailable", "Skills listing exceeds its byte bound.");
|
|
741
714
|
const entry = structuredClone(original);
|
|
742
715
|
validateEntry(entry);
|
|
@@ -758,14 +731,14 @@ export class SkillsRegistry {
|
|
|
758
731
|
if (records.some((record) => record.entry.uri === uri))
|
|
759
732
|
throw new ConnectorCallError("unavailable", "Downstream skills listing contains duplicate entries.");
|
|
760
733
|
const mapped = { ...entry, uri, resources };
|
|
761
|
-
catalogBytes +=
|
|
734
|
+
catalogBytes += utf8Length(JSON.stringify(mapped)) + 1;
|
|
762
735
|
if (catalogBytes > MAX_SKILL_CATALOG_BYTES)
|
|
763
736
|
throw new ConnectorCallError("unavailable", "Skills listing exceeds its byte bound.");
|
|
764
737
|
records.push({ entry: mapped, aliases: [], connector, files });
|
|
765
738
|
}
|
|
766
739
|
}
|
|
767
740
|
if (records.length > MAX_SKILLS ||
|
|
768
|
-
|
|
741
|
+
utf8Length(JSON.stringify(records.map((record) => record.entry))) > MAX_SKILL_CATALOG_BYTES) {
|
|
769
742
|
throw new ConnectorCallError("unavailable", "Skills listing exceeds its byte or entry bound.");
|
|
770
743
|
}
|
|
771
744
|
// Keep the old investigation guide behind every connector and downstream
|
package/dist/sqlite.d.ts
CHANGED
|
@@ -27,22 +27,3 @@ export declare function sqliteStorage(database: SqliteDatabase): KVStorage;
|
|
|
27
27
|
* older than `retentionDays`.
|
|
28
28
|
*/
|
|
29
29
|
export declare function sqliteActivityStore(database: SqliteDatabase, options?: SqliteActivityOptions): ActivityStore;
|
|
30
|
-
export interface StateFileImport {
|
|
31
|
-
/** Entries copied into the database. */
|
|
32
|
-
imported: number;
|
|
33
|
-
/** Entries left alone because the database already held their key. */
|
|
34
|
-
kept: number;
|
|
35
|
-
/** Entries whose TTL had already passed. */
|
|
36
|
-
expired: number;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* One-shot migration from the 0.28 `fileStorage` JSON state file: copy every
|
|
40
|
-
* live entry, with its expiry, into the database's key-value table. Run it
|
|
41
|
-
* once, with the old deployment stopped, before the first start on SQLite.
|
|
42
|
-
*
|
|
43
|
-
* Keys the database already holds are kept, so a second run copies nothing
|
|
44
|
-
* twice and never overwrites state written since. The file is read, never
|
|
45
|
-
* changed; delete it once the deployment is verified. A file that is not a
|
|
46
|
-
* state file throws before anything is written.
|
|
47
|
-
*/
|
|
48
|
-
export declare function importStateFile(database: SqliteDatabase, statePath: string, now?: number): StateFileImport;
|
package/dist/sqlite.js
CHANGED
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
// install. `sqliteStorage` is the deployment's `KVStorage` and
|
|
5
5
|
// `sqliteActivityStore` its activity history; both create their tables on
|
|
6
6
|
// first use. Node-only: nothing reachable from the root entry imports this.
|
|
7
|
-
import { chmodSync, closeSync, mkdirSync, openSync
|
|
7
|
+
import { chmodSync, closeSync, mkdirSync, openSync } from "node:fs";
|
|
8
8
|
import { dirname, resolve } from "node:path";
|
|
9
9
|
import { DatabaseSync } from "node:sqlite";
|
|
10
10
|
import { effectSqlDriver, sqlDriverEdge, sqlSync } from "./storage/sql-driver.js";
|
|
11
|
-
import {
|
|
12
|
-
import { KV_SCHEMA, sqlActivityStore, sqlStorage, } from "./storage/sql.js";
|
|
11
|
+
import { sqlActivityStore, sqlStorage, } from "./storage/sql.js";
|
|
13
12
|
/** Milliseconds a write waits for another process's write lock. */
|
|
14
13
|
const BUSY_TIMEOUT_MS = 5_000;
|
|
15
14
|
/**
|
|
@@ -100,83 +99,3 @@ export function sqliteStorage(database) {
|
|
|
100
99
|
export function sqliteActivityStore(database, options) {
|
|
101
100
|
return sqlActivityStore(sqliteDriver(database), "sqlite", options);
|
|
102
101
|
}
|
|
103
|
-
/**
|
|
104
|
-
* One-shot migration from the 0.28 `fileStorage` JSON state file: copy every
|
|
105
|
-
* live entry, with its expiry, into the database's key-value table. Run it
|
|
106
|
-
* once, with the old deployment stopped, before the first start on SQLite.
|
|
107
|
-
*
|
|
108
|
-
* Keys the database already holds are kept, so a second run copies nothing
|
|
109
|
-
* twice and never overwrites state written since. The file is read, never
|
|
110
|
-
* changed; delete it once the deployment is verified. A file that is not a
|
|
111
|
-
* state file throws before anything is written.
|
|
112
|
-
*/
|
|
113
|
-
export function importStateFile(database, statePath, now = Date.now()) {
|
|
114
|
-
const text = readFileSync(statePath, "utf8");
|
|
115
|
-
// The file holds stored state (tokens, sealed credentials), and these
|
|
116
|
-
// errors reach the operator's terminal, so they name the file and an entry's
|
|
117
|
-
// position, never a key or the parser's account, which quotes the text.
|
|
118
|
-
let loaded;
|
|
119
|
-
try {
|
|
120
|
-
loaded = JSON.parse(text);
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
throw new TypeError(`${statePath} is not a connecta state file: it is not valid JSON`);
|
|
124
|
-
}
|
|
125
|
-
if (loaded === null || typeof loaded !== "object" || Array.isArray(loaded)) {
|
|
126
|
-
throw new TypeError(`${statePath} is not a connecta state file`);
|
|
127
|
-
}
|
|
128
|
-
const entries = Object.entries(loaded);
|
|
129
|
-
for (const [index, [key, entry]] of entries.entries()) {
|
|
130
|
-
validateStorageKey(key);
|
|
131
|
-
const valid = entry !== null &&
|
|
132
|
-
typeof entry === "object" &&
|
|
133
|
-
typeof entry.value === "string" &&
|
|
134
|
-
(!("exp" in entry) || Number.isFinite(entry.exp));
|
|
135
|
-
if (!valid) {
|
|
136
|
-
throw new TypeError(`${statePath} is not a connecta state file: entry ${index + 1} has no string value`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
let db;
|
|
140
|
-
let transaction = false;
|
|
141
|
-
let step = "open the SQLite database";
|
|
142
|
-
const result = { imported: 0, kept: 0, expired: 0 };
|
|
143
|
-
try {
|
|
144
|
-
db = typeof database === "string" ? openSqlite(database) : database;
|
|
145
|
-
step = "begin the import transaction";
|
|
146
|
-
db.exec("BEGIN IMMEDIATE");
|
|
147
|
-
transaction = true;
|
|
148
|
-
step = "prepare the key-value table";
|
|
149
|
-
for (const statement of KV_SCHEMA)
|
|
150
|
-
db.exec(statement);
|
|
151
|
-
const insert = db.prepare(`INSERT INTO connecta_kv (key, value, expires_at_ms) VALUES (?, ?, ?)
|
|
152
|
-
ON CONFLICT (key) DO NOTHING`);
|
|
153
|
-
for (const [index, [key, entry]] of entries.entries()) {
|
|
154
|
-
step = `import entry ${index + 1} into the key-value table`;
|
|
155
|
-
const { value, exp } = entry;
|
|
156
|
-
if (exp !== undefined && exp <= now) {
|
|
157
|
-
result.expired += 1;
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
if (Number(insert.run(key, value, exp === undefined ? null : Math.ceil(exp)).changes) > 0) {
|
|
161
|
-
result.imported += 1;
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
result.kept += 1;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
step = "commit the import transaction";
|
|
168
|
-
db.exec("COMMIT");
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
if (transaction) {
|
|
172
|
-
try {
|
|
173
|
-
db.exec("ROLLBACK");
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
// Rollback errors can contain stored text too. Report the failed step.
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
throw new Error(`State import failed: could not ${step}`);
|
|
180
|
-
}
|
|
181
|
-
return result;
|
|
182
|
-
}
|