@zackbart/connecta 0.37.1 → 0.39.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 +94 -0
- package/README.md +5 -4
- 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/agent-routes.js +231 -0
- package/dist/auth/clerk-sdk/generated.js +2 -1
- package/dist/auth/downstream-oauth/consent.js +519 -0
- package/dist/auth/downstream-oauth/grant-store.js +439 -0
- package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
- package/dist/auth/downstream-oauth/refresh-records.js +447 -0
- package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
- package/dist/auth/downstream-oauth.js +66 -2516
- package/dist/auth/downstream-revocation.js +1 -1
- package/dist/auth/native.js +27 -0
- package/dist/auth/oauth-grant.js +24 -0
- package/dist/auth/oauth-token-response.js +1 -1
- package/dist/auth/static-oauth.js +153 -131
- package/dist/auth/vercel-agent.js +2 -1
- package/dist/auth-elicitation.js +9 -3
- package/dist/authorization-handoff.js +11 -5
- 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-cache.js +130 -43
- package/dist/catalog-fingerprint.js +2 -18
- package/dist/catalog-policy.js +18 -0
- package/dist/catalog-service.js +285 -48
- package/dist/catalog.js +18 -2
- 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 +57 -25
- package/dist/config.js +30 -77
- package/dist/connector-caller.d.ts +1 -1
- package/dist/connector-health.js +59 -0
- package/dist/connector-status.js +117 -0
- package/dist/connectors/api-connector.d.ts +7 -2
- package/dist/connectors/api-connector.js +11 -7
- package/dist/connectors/guarded-fetch.d.ts +0 -6
- package/dist/connectors/guarded-fetch.js +1 -11
- package/dist/connectors/negotiation-cache.js +22 -4
- package/dist/connectors/remote-mcp/call.js +164 -0
- package/dist/connectors/remote-mcp/catalog.js +278 -0
- package/dist/connectors/remote-mcp/config.js +263 -0
- package/dist/connectors/remote-mcp/connection.js +671 -0
- package/dist/connectors/remote-mcp/errors.js +488 -0
- package/dist/connectors/remote-mcp/oauth.js +469 -0
- package/dist/connectors/remote-mcp/resources.js +473 -0
- package/dist/connectors/remote-mcp/runtime.js +1 -0
- package/dist/connectors/remote-mcp/transport.js +544 -0
- package/dist/connectors/remote-mcp.js +61 -2898
- package/dist/connectors/resource-uri.js +0 -4
- package/dist/credential-contract.d.ts +23 -0
- package/dist/credential-reads.js +17 -0
- package/dist/credential-verification.js +53 -0
- package/dist/credentials.d.ts +7 -0
- package/dist/credentials.js +60 -25
- package/dist/d1.d.ts +0 -89
- package/dist/d1.js +0 -34
- package/dist/describe-config.d.ts +12 -2
- package/dist/describe-config.js +2 -0
- package/dist/discovery-health.js +199 -0
- package/dist/downstream-input.js +4 -2
- package/dist/errors.d.ts +55 -17
- package/dist/errors.js +95 -41
- package/dist/execute.js +172 -125
- package/dist/executor-result.js +5 -2
- package/dist/executors/quickjs-runtime.js +2 -25
- package/dist/executors/quickjs.js +1 -1
- package/dist/guest-functions.js +83 -0
- package/dist/guest-runtime.js +63 -20
- package/dist/guest-types.d.ts +24 -10
- package/dist/http/argument-problems.js +35 -0
- package/dist/http/client.js +1 -1
- package/dist/http/errors.js +7 -6
- package/dist/http/fields.js +192 -13
- package/dist/http/hatch-response.js +73 -0
- package/dist/http/hatch.js +88 -23
- package/dist/http/json-schema.js +1 -1
- package/dist/http/node-transport.js +0 -1
- package/dist/http/oauth.js +4 -3
- package/dist/http/paging.js +6 -9
- package/dist/http/provider.js +33 -5
- package/dist/http/safety.js +9 -1
- package/dist/http/services.js +1 -8
- package/dist/http/tools.js +45 -30
- package/dist/http/vendor.js +7 -7
- package/dist/identity.js +2 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/invocation-auth.js +2 -1
- package/dist/invocation.js +21 -6
- package/dist/membership.d.ts +13 -5
- package/dist/membership.js +34 -2
- package/dist/meta-tools.js +50 -35
- package/dist/native-recovery.js +4 -16
- package/dist/native-tools.js +6 -2
- package/dist/oauth-handoff.js +2 -2
- package/dist/oauth-sealing.js +10 -0
- package/dist/operation-vocabulary.js +136 -14
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -49
- package/dist/operator-ui/contract.d.ts +12 -2
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/model.d.ts +22 -1
- 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 +49 -53
- package/dist/providers/_shared/rest/value-safety.js +106 -28
- package/dist/providers/breeze/api.js +9 -8
- package/dist/providers/breeze/index.js +4 -2
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/breeze/tools.js +7 -7
- package/dist/providers/ccb/api.js +3 -7
- package/dist/providers/ccb/index.js +7 -1
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/ccb/tools.js +1 -6
- package/dist/providers/cloudflare/api.js +10 -3
- package/dist/providers/cloudflare/index.js +2 -1
- package/dist/providers/cloudflare/safety.js +3 -3
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/lib/schemas.js +24 -22
- package/dist/providers/docs/safety.js +2 -0
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/docs/tools.js +4 -14
- package/dist/providers/drive/api.js +1 -0
- package/dist/providers/drive/lib/contracts.js +8 -5
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/drive/tools.js +23 -41
- package/dist/providers/forms/api.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/forms/tools.js +11 -16
- package/dist/providers/github/api.js +1 -0
- package/dist/providers/github/lib/discovery.js +1 -1
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +61 -17
- package/dist/providers/gmail/lib/schemas.js +60 -50
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/gmail/tools.js +12 -26
- package/dist/providers/granola/index.js +2 -0
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/api.js +8 -4
- package/dist/providers/overflow/index.js +2 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/overflow/tools.js +3 -7
- package/dist/providers/planning-center/api.js +5 -2
- 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/skill.generated.js +1 -1
- package/dist/providers/planning-center/tools.js +2 -1
- package/dist/providers/quickbooks/safety.js +80 -39
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/api.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/sheets/tools.js +14 -23
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/slides/tools.js +19 -26
- package/dist/providers/stripe/index.js +2 -1
- package/dist/providers/stripe/safety.js +2 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/api.js +5 -8
- package/dist/providers/tithely/index.js +3 -2
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/tithely/tools.js +7 -5
- package/dist/providers/vercel/api.js +2 -7
- package/dist/providers/vercel/lib/pin.js +1 -1
- package/dist/providers/vercel/safety.js +1 -1
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/tools.js +6 -4
- package/dist/redaction-marker.js +2 -2
- package/dist/registry.d.ts +11 -5
- package/dist/registry.js +46 -20
- 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 +130 -61
- package/dist/routes/dispatch.js +5 -1
- package/dist/routes/mcp.js +34 -17
- package/dist/routes/oauth-management.js +1 -1
- package/dist/routes/operator.js +6 -5
- package/dist/routes/shared.d.ts +13 -8
- package/dist/routes/shared.js +15 -9
- package/dist/routes/ui-config.js +3 -3
- package/dist/routes/ui-data.js +126 -20
- package/dist/routes/ui.js +17 -1
- package/dist/runtime/call-admission.js +1 -1
- package/dist/runtime/call.js +8 -3
- package/dist/runtime/native-storage.js +9 -0
- package/dist/runtime/request-reads.js +182 -0
- package/dist/runtime/request-scope.js +16 -0
- package/dist/runtime/settlement.js +1 -1
- package/dist/runtime/shared-read.js +2 -15
- package/dist/runtime/storage.js +12 -0
- 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 +44 -54
- package/dist/sqlite.d.ts +0 -19
- package/dist/sqlite.js +2 -83
- package/dist/storage/keys.js +69 -128
- package/dist/storage/memory.js +8 -2
- package/dist/storage/sql-native.js +27 -101
- package/dist/storage/sql.d.ts +1 -24
- package/dist/storage/sql.js +13 -14
- package/dist/storage-digest.js +32 -0
- package/dist/timeout.js +8 -0
- package/dist/tool-error.js +4 -0
- package/dist/types.d.ts +33 -0
- package/dist/url-safety.js +0 -4
- package/dist/usage-guide.js +18 -37
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +220 -158
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +1 -1
- package/dist/write-outcome.js +0 -3
- package/documentation/README.md +19 -55
- package/documentation/architecture.md +436 -515
- package/documentation/auth.md +229 -88
- package/documentation/code-mode.md +201 -94
- package/documentation/deploying.md +25 -43
- package/documentation/integrating.md +18 -19
- package/documentation/meta-tools.md +225 -51
- package/documentation/migration-0.33.md +10 -6
- package/documentation/migration-0.34.md +5 -5
- package/documentation/migration-retired-layouts.md +74 -0
- package/documentation/operator-tests.md +20 -120
- package/documentation/operator-ui.md +108 -16
- package/documentation/pool-membership-upgrade.md +44 -0
- package/documentation/provider-auth-migration.md +4 -10
- package/documentation/provider-migration-0.29.md +4 -15
- package/documentation/providers.md +105 -0
- package/examples/worker/AGENTS.md +14 -16
- package/examples/worker/README.md +24 -118
- package/examples/worker/src/connecta.config.ts +1 -0
- package/guides/connecting-agents.md +9 -31
- package/guides/how-it-works.md +6 -4
- package/guides/safety-and-trust.md +4 -1
- package/package.json +19 -19
- package/templates/node/AGENTS.md +13 -12
- package/templates/node/README.md +9 -26
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +1 -0
- 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
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
|
-
}
|
package/dist/storage/keys.js
CHANGED
|
@@ -141,7 +141,7 @@ export const catalogKeys = {
|
|
|
141
141
|
name: "catalog",
|
|
142
142
|
scope: "root",
|
|
143
143
|
prefixes: ["catalog:"],
|
|
144
|
-
// Retired v2/v3 listings
|
|
144
|
+
// Retired v2/v3 listings expire on their original TTL; no runtime reads them.
|
|
145
145
|
version: { number: 3, in: "value" },
|
|
146
146
|
codec: textCodec,
|
|
147
147
|
ttl: {
|
|
@@ -153,7 +153,33 @@ export const catalogKeys = {
|
|
|
153
153
|
manifest: (connectorId) => validateStorageKey(`catalog:${connectorId}`),
|
|
154
154
|
chunk: (connectorId, revision, index) => validateStorageKey(`catalog:${connectorId}:chunk:${revision}:${index}`),
|
|
155
155
|
};
|
|
156
|
-
/**
|
|
156
|
+
/** How long a connector-health record outlives its retry time, fencing the next probe's claim. */
|
|
157
|
+
export const CONNECTOR_HEALTH_LINGER_MS = 60_000;
|
|
158
|
+
/**
|
|
159
|
+
* Bounded connector health by connector id and a digest of the owner
|
|
160
|
+
* partition and described configuration. `discovery` holds the latest failed
|
|
161
|
+
* discovery's typed code, observation time, retry time and the catalog
|
|
162
|
+
* generation it saw; `status` the operator page's latest status observation
|
|
163
|
+
* (state, problem code, tool count, observation time, generation). Values
|
|
164
|
+
* never carry downstream text, tools or credentials.
|
|
165
|
+
*/
|
|
166
|
+
export const connectorHealthKeys = {
|
|
167
|
+
family: {
|
|
168
|
+
name: "connector-health",
|
|
169
|
+
scope: "root",
|
|
170
|
+
prefixes: ["connector-health:v1:"],
|
|
171
|
+
version: { number: 1, in: "key" },
|
|
172
|
+
codec: jsonCodec,
|
|
173
|
+
ttl: { kind: "configured", by: "discovery: retry time plus 60 seconds; status: 120 seconds" },
|
|
174
|
+
durable: false,
|
|
175
|
+
},
|
|
176
|
+
discovery: (connectorId, partitionDigest) => validateStorageKey(`connector-health:v1:${connectorId}:${partitionDigest}:discovery`),
|
|
177
|
+
status: (connectorId, partitionDigest) => validateStorageKey(`connector-health:v1:${connectorId}:${partitionDigest}:status`),
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* SDK catalog response cache, complete manifests published after their chunks.
|
|
181
|
+
* Secret-bearing inputs reach these keys only through `secretDigest`.
|
|
182
|
+
*/
|
|
157
183
|
export const responseCacheKeys = {
|
|
158
184
|
family: {
|
|
159
185
|
name: "response-cache",
|
|
@@ -242,38 +268,6 @@ export const credentialKeys = {
|
|
|
242
268
|
},
|
|
243
269
|
credential: (connectorId, owner) => `${owner ? scopes.principal(owner) : ""}${scopes.connector(connectorId)}credential:v1`,
|
|
244
270
|
};
|
|
245
|
-
/** Seconds a Workers KV → D1 copy's resume point stays usable. */
|
|
246
|
-
export const KV_COPY_CURSOR_TTL_SECONDS = 7 * 24 * 60 * 60;
|
|
247
|
-
/**
|
|
248
|
-
* Where a one-shot Workers KV → D1 copy (`copyKvToD1` in `src/d1.ts`) resumes,
|
|
249
|
-
* held in D1 under a random token. Workers KV's own list cursor can spell the
|
|
250
|
-
* last key it listed, so the caller holds the token and never the cursor.
|
|
251
|
-
*/
|
|
252
|
-
export const kvCopyKeys = {
|
|
253
|
-
family: {
|
|
254
|
-
name: "kv-copy",
|
|
255
|
-
scope: "root",
|
|
256
|
-
prefixes: ["kv-copy:v1:"],
|
|
257
|
-
version: { number: 1, in: "key" },
|
|
258
|
-
codec: textCodec,
|
|
259
|
-
ttl: { kind: "fixed", seconds: KV_COPY_CURSOR_TTL_SECONDS },
|
|
260
|
-
durable: false,
|
|
261
|
-
},
|
|
262
|
-
cursor: (token) => validateStorageKey(`kv-copy:v1:cursor:${token}`),
|
|
263
|
-
};
|
|
264
|
-
/** Once traffic uses D1, copying stale KV state is refused. */
|
|
265
|
-
export const kvCutoverKeys = {
|
|
266
|
-
family: {
|
|
267
|
-
name: "kv-cutover",
|
|
268
|
-
scope: "root",
|
|
269
|
-
prefixes: ["kv-cutover:v1:"],
|
|
270
|
-
version: { number: 1, in: "key" },
|
|
271
|
-
codec: textCodec,
|
|
272
|
-
ttl: { kind: "durable" },
|
|
273
|
-
durable: true,
|
|
274
|
-
},
|
|
275
|
-
source: (source) => validateStorageKey(`kv-cutover:v1:${encodeURIComponent(source)}`),
|
|
276
|
-
};
|
|
277
271
|
// --- connector families ---------------------------------------------------
|
|
278
272
|
/** Seconds a consent's flow record stays claimable, the same as its link. */
|
|
279
273
|
export const OAUTH_FLOW_TTL_SECONDS = 15 * 60;
|
|
@@ -356,38 +350,6 @@ export const oauthRefreshActiveKeys = {
|
|
|
356
350
|
prefix: "oauth:refresh-active:",
|
|
357
351
|
holder: (epoch, holder) => validateStorageKey(`oauth:refresh-active:${epoch}:${holder}`),
|
|
358
352
|
};
|
|
359
|
-
/** The values layout 2 stored, each under its historical key. */
|
|
360
|
-
const oauthV2Field = {
|
|
361
|
-
client: "oauth:client",
|
|
362
|
-
tokens: "oauth:tokens",
|
|
363
|
-
pending: "oauth:pending",
|
|
364
|
-
verifier: "oauth:verifier",
|
|
365
|
-
state: "oauth:state",
|
|
366
|
-
discovery: "oauth:discovery",
|
|
367
|
-
};
|
|
368
|
-
/**
|
|
369
|
-
* Layout 2, read once to migrate it into a grant record and then deleted:
|
|
370
|
-
* each value at its field key, suffixed `:epoch:<generation>` once a modern
|
|
371
|
-
* generation owned it; the active generation at `oauth:generation`; a reset's
|
|
372
|
-
* cleanup lineage at `oauth:cleanup:` and `oauth:cleanup-at:`.
|
|
373
|
-
*/
|
|
374
|
-
export const oauthV2Keys = {
|
|
375
|
-
family: {
|
|
376
|
-
name: "oauth-v2",
|
|
377
|
-
scope: "connector",
|
|
378
|
-
prefixes: [...Object.values(oauthV2Field), "oauth:generation", "oauth:cleanup:", "oauth:cleanup-at:"],
|
|
379
|
-
version: { number: 2, in: "value" },
|
|
380
|
-
codec: textCodec,
|
|
381
|
-
ttl: { kind: "durable" },
|
|
382
|
-
durable: true,
|
|
383
|
-
},
|
|
384
|
-
field: oauthV2Field,
|
|
385
|
-
/** One listing covers every layout 2 key; the reader keeps those above. */
|
|
386
|
-
scan: "oauth:",
|
|
387
|
-
/** A value's physical key; `epoch` null is the historical unsuffixed name. */
|
|
388
|
-
value: (key, epoch) => validateStorageKey(epoch === null ? key : `${key}:epoch:${epoch}`),
|
|
389
|
-
generation: "oauth:generation",
|
|
390
|
-
};
|
|
391
353
|
/** Single-use browser links (`used`/`started`/`failed`/`closed`) and MRTR rounds. */
|
|
392
354
|
export const oauthConnectKeys = {
|
|
393
355
|
family: {
|
|
@@ -403,8 +365,31 @@ export const oauthConnectKeys = {
|
|
|
403
365
|
used: (nonce) => validateStorageKey(`oauth:connect-used:${nonce}`),
|
|
404
366
|
retry: (nonce) => validateStorageKey(`oauth:request-used:${nonce}`),
|
|
405
367
|
};
|
|
406
|
-
/**
|
|
368
|
+
/**
|
|
369
|
+
* The last verification of a connector's operator-managed credential:
|
|
370
|
+
* `verification:v1`, holding an outcome class, its time and the vault
|
|
371
|
+
* revision of the save it observed. Payload-free and long-lived; a lost
|
|
372
|
+
* record only makes the operator page say "not verified".
|
|
373
|
+
*/
|
|
374
|
+
export const verificationKeys = {
|
|
375
|
+
family: {
|
|
376
|
+
name: "verification",
|
|
377
|
+
scope: "connector",
|
|
378
|
+
prefixes: ["verification:v1"],
|
|
379
|
+
version: { number: 1, in: "key" },
|
|
380
|
+
codec: jsonCodec,
|
|
381
|
+
ttl: { kind: "durable" },
|
|
382
|
+
durable: false,
|
|
383
|
+
},
|
|
384
|
+
record: "verification:v1",
|
|
385
|
+
};
|
|
386
|
+
/** A bounded, credential-partitioned downstream protocol verdict, keyed through `secretDigest`. */
|
|
407
387
|
export const NEGOTIATION_TTL_SECONDS = 300;
|
|
388
|
+
/**
|
|
389
|
+
* A legacy verdict lives longer: a server that upgrades still answers the
|
|
390
|
+
* legacy handshake, and one that stops answering it is renegotiated at once.
|
|
391
|
+
*/
|
|
392
|
+
export const LEGACY_NEGOTIATION_TTL_SECONDS = 6 * 60 * 60;
|
|
408
393
|
export const negotiationKeys = {
|
|
409
394
|
family: {
|
|
410
395
|
name: "negotiation",
|
|
@@ -412,7 +397,8 @@ export const negotiationKeys = {
|
|
|
412
397
|
prefixes: ["negotiation:v1:"],
|
|
413
398
|
version: { number: 1, in: "key" },
|
|
414
399
|
codec: jsonCodec,
|
|
415
|
-
|
|
400
|
+
// The longest verdict lifetime; a modern verdict expires sooner.
|
|
401
|
+
ttl: { kind: "fixed", seconds: LEGACY_NEGOTIATION_TTL_SECONDS },
|
|
416
402
|
durable: false,
|
|
417
403
|
},
|
|
418
404
|
verdict: (partitionDigest) => validateStorageKey(`negotiation:v1:${partitionDigest}`),
|
|
@@ -430,63 +416,18 @@ export const inputRetryKeys = {
|
|
|
430
416
|
},
|
|
431
417
|
used: (nonce) => validateStorageKey(`input-retry:v1:${nonce}`),
|
|
432
418
|
};
|
|
433
|
-
/**
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
accessTokenKeys.family,
|
|
449
|
-
agentObservationKeys.family,
|
|
450
|
-
credentialKeys.family,
|
|
451
|
-
oauthGrantKeys.family,
|
|
452
|
-
oauthFlowKeys.family,
|
|
453
|
-
oauthRefreshKeys.family,
|
|
454
|
-
oauthRefreshSpentKeys.family,
|
|
455
|
-
oauthRefreshActiveKeys.family,
|
|
456
|
-
oauthV2Keys.family,
|
|
457
|
-
oauthConnectKeys.family,
|
|
458
|
-
negotiationKeys.family,
|
|
459
|
-
inputRetryKeys.family,
|
|
460
|
-
kvCopyKeys.family,
|
|
461
|
-
kvCutoverKeys.family,
|
|
462
|
-
];
|
|
463
|
-
// Principal, connector, and subject segments are hex digests and connector
|
|
464
|
-
// ids, so none holds a colon.
|
|
465
|
-
const PRINCIPAL_SCOPE = /^principal:[^:]*:/;
|
|
466
|
-
const CONNECTOR_SCOPE = /^conn:[^:]*:/;
|
|
467
|
-
const PARTITION_SCOPE = /^(?:results:|subject:[^:]*:)/;
|
|
468
|
-
/**
|
|
469
|
-
* The family a physical key belongs to, for reports that count keys without
|
|
470
|
-
* naming them: one-shot migrations report per family, never per key. A key
|
|
471
|
-
* no family claims is `connector-owned` when a custom connector wrote it
|
|
472
|
-
* through its own `ctx.storage`, and `unclassified` otherwise (a key an older release wrote).
|
|
473
|
-
*/
|
|
474
|
-
export function familyOfKey(key) {
|
|
475
|
-
let rest = key.replace(PRINCIPAL_SCOPE, "");
|
|
476
|
-
let scope = "root";
|
|
477
|
-
for (const [pattern, nested] of [
|
|
478
|
-
[CONNECTOR_SCOPE, "connector"],
|
|
479
|
-
[PARTITION_SCOPE, "partition"],
|
|
480
|
-
]) {
|
|
481
|
-
const match = pattern.exec(rest);
|
|
482
|
-
if (match) {
|
|
483
|
-
rest = rest.slice(match[0].length);
|
|
484
|
-
scope = nested;
|
|
485
|
-
break;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
const family = KEY_FAMILIES.find((candidate) => candidate.scope === scope && candidate.prefixes.some((prefix) => rest.startsWith(prefix)));
|
|
489
|
-
if (family)
|
|
490
|
-
return family.name;
|
|
491
|
-
return scope === "connector" ? "connector-owned" : "unclassified";
|
|
492
|
-
}
|
|
419
|
+
/** Retired OAuth names are inspected only to report that fresh consent is required. */
|
|
420
|
+
export const retiredOAuthKeys = {
|
|
421
|
+
scan: "oauth:",
|
|
422
|
+
prefixes: [
|
|
423
|
+
"oauth:client",
|
|
424
|
+
"oauth:tokens",
|
|
425
|
+
"oauth:pending",
|
|
426
|
+
"oauth:verifier",
|
|
427
|
+
"oauth:state",
|
|
428
|
+
"oauth:discovery",
|
|
429
|
+
"oauth:generation",
|
|
430
|
+
"oauth:cleanup:",
|
|
431
|
+
"oauth:cleanup-at:",
|
|
432
|
+
],
|
|
433
|
+
};
|
package/dist/storage/memory.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validateStorageKey } from "./keys.js";
|
|
2
|
+
import { noteStorageWrite, observeStorageWrites } from "../runtime/request-reads.js";
|
|
2
3
|
/** In-memory KV store with expiry. The default for dev and Node. */
|
|
3
4
|
export function memoryStorage() {
|
|
4
5
|
const map = new Map();
|
|
@@ -40,7 +41,9 @@ export function memoryStorage() {
|
|
|
40
41
|
...(exp !== undefined ? { exp } : {}),
|
|
41
42
|
});
|
|
42
43
|
};
|
|
43
|
-
|
|
44
|
+
// Each write settles synchronously, so one note covers it (request-reads.ts).
|
|
45
|
+
const root = {};
|
|
46
|
+
return observeStorageWrites({
|
|
44
47
|
capabilities: { absoluteExpiry: true },
|
|
45
48
|
describe: () => ({ kind: "memory" }),
|
|
46
49
|
async get(key) {
|
|
@@ -49,10 +52,12 @@ export function memoryStorage() {
|
|
|
49
52
|
},
|
|
50
53
|
async set(key, value, opts) {
|
|
51
54
|
validateStorageKey(key);
|
|
55
|
+
noteStorageWrite(root, key);
|
|
52
56
|
write(key, value, opts);
|
|
53
57
|
},
|
|
54
58
|
async delete(key) {
|
|
55
59
|
validateStorageKey(key);
|
|
60
|
+
noteStorageWrite(root, key);
|
|
56
61
|
map.delete(key);
|
|
57
62
|
},
|
|
58
63
|
async list(prefix) {
|
|
@@ -68,6 +73,7 @@ export function memoryStorage() {
|
|
|
68
73
|
// runs to completion before any other call on this map can start.
|
|
69
74
|
async compareAndSet(key, expected, next, opts) {
|
|
70
75
|
validateStorageKey(key);
|
|
76
|
+
noteStorageWrite(root, key);
|
|
71
77
|
if ((fresh(key)?.value ?? null) !== expected)
|
|
72
78
|
return false;
|
|
73
79
|
if (next === null)
|
|
@@ -76,5 +82,5 @@ export function memoryStorage() {
|
|
|
76
82
|
write(key, next, opts);
|
|
77
83
|
return true;
|
|
78
84
|
},
|
|
79
|
-
};
|
|
85
|
+
}, { root });
|
|
80
86
|
}
|
|
@@ -61,6 +61,8 @@ function schemaOnce(driver, ddl) {
|
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
// --- key-value ----------------------------------------------------------
|
|
64
|
+
/** Keys one `getMany` statement binds. */
|
|
65
|
+
const GET_MANY_KEYS = 64;
|
|
64
66
|
/** Expired rows one write removes on its way. */
|
|
65
67
|
const SWEEP_ROWS = 16;
|
|
66
68
|
function ttlMillis(ttlSeconds) {
|
|
@@ -105,6 +107,31 @@ export function nativeSqlStorage(driver) {
|
|
|
105
107
|
return yield* current(key);
|
|
106
108
|
});
|
|
107
109
|
},
|
|
110
|
+
getMany(keys) {
|
|
111
|
+
return Effect.gen(function* () {
|
|
112
|
+
for (const key of keys)
|
|
113
|
+
validateStorageKey(key);
|
|
114
|
+
if (keys.length === 0)
|
|
115
|
+
return [];
|
|
116
|
+
yield* ensure();
|
|
117
|
+
// One statement for every key: a chunked value costs one round trip.
|
|
118
|
+
// Rows answer by position, never by the key text a driver returns.
|
|
119
|
+
// Bound parameters stay under D1's per-statement limit of 100.
|
|
120
|
+
const values = keys.map(() => null);
|
|
121
|
+
for (let offset = 0; offset < keys.length; offset += GET_MANY_KEYS) {
|
|
122
|
+
const batch = keys.slice(offset, offset + GET_MANY_KEYS);
|
|
123
|
+
const rows = yield* driver.all({
|
|
124
|
+
sql: `WITH wanted (position, key) AS (VALUES ${batch.map((_, i) => `(${offset + i}, ?)`).join(", ")})
|
|
125
|
+
SELECT wanted.position AS position, ${textColumn("value")}
|
|
126
|
+
FROM wanted JOIN connecta_kv ON connecta_kv.key = wanted.key AND ${live}`,
|
|
127
|
+
params: batch,
|
|
128
|
+
});
|
|
129
|
+
for (const row of rows)
|
|
130
|
+
values[row.position] = textOf(row.value, row.value_bytes, `the stored value of ${JSON.stringify(keys[row.position])}`);
|
|
131
|
+
}
|
|
132
|
+
return values;
|
|
133
|
+
});
|
|
134
|
+
},
|
|
108
135
|
set(key, value, opts) {
|
|
109
136
|
return Effect.gen(function* () {
|
|
110
137
|
validateStorageKey(key);
|
|
@@ -177,107 +204,6 @@ export function nativeSqlStorage(driver) {
|
|
|
177
204
|
},
|
|
178
205
|
};
|
|
179
206
|
}
|
|
180
|
-
/** Keys one read compares: D1 binds at most 100 parameters, one is `now`. */
|
|
181
|
-
const COPY_READ_KEYS = 99;
|
|
182
|
-
/** Writes one transaction carries, by count and by UTF-8 bytes. */
|
|
183
|
-
const COPY_BATCH_STATEMENTS = 50;
|
|
184
|
-
const COPY_BATCH_BYTES = 4 * 1024 * 1024;
|
|
185
|
-
/**
|
|
186
|
-
* Copy entries into the key-value table, verbatim: same key, same value,
|
|
187
|
-
* same absolute expiry. A live row holding a different value is kept unless
|
|
188
|
-
* `overwrite` is set, so a second copy of the same entries writes nothing.
|
|
189
|
-
* Writes go out in bounded transactions; the outcomes follow `entries`.
|
|
190
|
-
*/
|
|
191
|
-
export function copyIntoSqlEffect(driver, entries, options) {
|
|
192
|
-
return Effect.gen(function* () {
|
|
193
|
-
const { overwrite, now } = options;
|
|
194
|
-
for (const entry of entries)
|
|
195
|
-
validateStorageKey(entry.key);
|
|
196
|
-
yield* driver.batch(KV_SCHEMA.map((statement) => sql(statement)));
|
|
197
|
-
const live = "(expires_at_ms IS NULL OR expires_at_ms > ?)";
|
|
198
|
-
const currentValues = (keys) => Effect.gen(function* () {
|
|
199
|
-
const values = new Map();
|
|
200
|
-
for (let start = 0; start < keys.length; start += COPY_READ_KEYS) {
|
|
201
|
-
const chunk = keys.slice(start, start + COPY_READ_KEYS);
|
|
202
|
-
const rows = yield* driver.all(sql(`SELECT key, ${textColumn("value")}, expires_at_ms FROM connecta_kv
|
|
203
|
-
WHERE key IN (${chunk.map(() => "?").join(", ")}) AND ${live}`, ...chunk, now));
|
|
204
|
-
// The message names no key: a copy's errors reach operator output.
|
|
205
|
-
for (const row of rows) {
|
|
206
|
-
values.set(row.key, {
|
|
207
|
-
value: textOf(row.value, row.value_bytes, "a stored value") ?? "",
|
|
208
|
-
expiresAtMs: row.expires_at_ms,
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return values;
|
|
213
|
-
});
|
|
214
|
-
const outcomes = Array.from({ length: entries.length });
|
|
215
|
-
const writes = [];
|
|
216
|
-
const existing = yield* currentValues(entries.map((entry) => entry.key));
|
|
217
|
-
for (const [index, entry] of entries.entries()) {
|
|
218
|
-
const current = existing.get(entry.key);
|
|
219
|
-
if (current?.value === entry.value && current.expiresAtMs === entry.expiresAtMs) {
|
|
220
|
-
outcomes[index] = "unchanged";
|
|
221
|
-
}
|
|
222
|
-
else if (current !== undefined && !overwrite) {
|
|
223
|
-
outcomes[index] = "conflict";
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
outcomes[index] = current === undefined ? "copied" : "overwritten";
|
|
227
|
-
writes.push({
|
|
228
|
-
index,
|
|
229
|
-
bytes: new TextEncoder().encode(entry.key).byteLength + new TextEncoder().encode(entry.value).byteLength + 32,
|
|
230
|
-
statement: overwrite
|
|
231
|
-
? sql(`INSERT INTO connecta_kv (key, value, expires_at_ms)
|
|
232
|
-
VALUES (?, ?, ?)
|
|
233
|
-
ON CONFLICT (key) DO UPDATE SET
|
|
234
|
-
value = excluded.value, expires_at_ms = excluded.expires_at_ms`, entry.key, entry.value, entry.expiresAtMs)
|
|
235
|
-
: // Absent when read; a live row written since is kept, not replaced.
|
|
236
|
-
sql(`INSERT INTO connecta_kv (key, value, expires_at_ms)
|
|
237
|
-
VALUES (?, ?, ?)
|
|
238
|
-
ON CONFLICT (key) DO UPDATE SET
|
|
239
|
-
value = excluded.value, expires_at_ms = excluded.expires_at_ms
|
|
240
|
-
WHERE connecta_kv.expires_at_ms IS NOT NULL
|
|
241
|
-
AND connecta_kv.expires_at_ms <= ?`, entry.key, entry.value, entry.expiresAtMs, now),
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
const raced = [];
|
|
246
|
-
let batch = [];
|
|
247
|
-
let batchBytes = 0;
|
|
248
|
-
const flush = () => Effect.gen(function* () {
|
|
249
|
-
if (batch.length === 0)
|
|
250
|
-
return;
|
|
251
|
-
const changes = yield* driver.batch(batch.map((write) => write.statement));
|
|
252
|
-
for (const [position, write] of batch.entries()) {
|
|
253
|
-
if (!(changes[position] > 0))
|
|
254
|
-
raced.push(write.index);
|
|
255
|
-
}
|
|
256
|
-
batch = [];
|
|
257
|
-
batchBytes = 0;
|
|
258
|
-
});
|
|
259
|
-
for (const write of writes) {
|
|
260
|
-
if (batch.length > 0 && (batch.length >= COPY_BATCH_STATEMENTS || batchBytes + write.bytes > COPY_BATCH_BYTES)) {
|
|
261
|
-
yield* flush();
|
|
262
|
-
}
|
|
263
|
-
batch.push(write);
|
|
264
|
-
batchBytes += write.bytes;
|
|
265
|
-
}
|
|
266
|
-
yield* flush();
|
|
267
|
-
// A row another writer added between the read and the write: report what
|
|
268
|
-
// it holds, and leave it.
|
|
269
|
-
if (raced.length > 0) {
|
|
270
|
-
const after = yield* currentValues(raced.map((index) => entries[index].key));
|
|
271
|
-
for (const index of raced) {
|
|
272
|
-
const entry = entries[index];
|
|
273
|
-
const current = after.get(entry.key);
|
|
274
|
-
outcomes[index] =
|
|
275
|
-
current?.value === entry.value && current.expiresAtMs === entry.expiresAtMs ? "unchanged" : "conflict";
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return outcomes;
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
207
|
// --- activity -----------------------------------------------------------
|
|
282
208
|
/**
|
|
283
209
|
* The activity table, with keyset paging on `(occurred_at_ms, id)`. Its shape
|
package/dist/storage/sql.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { ActivityStore } from "../activity.js";
|
|
2
2
|
import type { KVStorage } from "../types.js";
|
|
3
|
-
/** The 0.28-compatible key-value table and expiry index. */
|
|
4
|
-
export { KV_SCHEMA } from "./sql-schema.js";
|
|
5
3
|
type SqlValue = string | number | null;
|
|
6
4
|
export interface SqlStatement {
|
|
7
5
|
readonly sql: string;
|
|
@@ -21,18 +19,6 @@ export interface SqlDriver {
|
|
|
21
19
|
}
|
|
22
20
|
/** The two drivers, as `describe()` names them. */
|
|
23
21
|
export type SqlKind = "d1" | "sqlite";
|
|
24
|
-
export interface SqlCopyEntry {
|
|
25
|
-
readonly key: string;
|
|
26
|
-
readonly value: string;
|
|
27
|
-
/** Epoch milliseconds; null never expires. */
|
|
28
|
-
readonly expiresAtMs: number | null;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* What a copy did with one entry: written where no live row was, left alone
|
|
32
|
-
* because the live row already held the value, refused because it held
|
|
33
|
-
* another (`conflict`), or replaced under `overwrite` (`overwritten`).
|
|
34
|
-
*/
|
|
35
|
-
export type SqlCopyOutcome = "copied" | "unchanged" | "conflict" | "overwritten";
|
|
36
22
|
export interface SqlActivityOptions {
|
|
37
23
|
/**
|
|
38
24
|
* Days of history kept. Each write removes a bounded batch of older rows,
|
|
@@ -48,13 +34,4 @@ export declare function sqlStorage(driver: SqlDriver, kind: SqlKind): KVStorage;
|
|
|
48
34
|
* payload to leak.
|
|
49
35
|
*/
|
|
50
36
|
export declare function sqlActivityStore(driver: SqlDriver, kind: SqlKind, options?: SqlActivityOptions): ActivityStore;
|
|
51
|
-
|
|
52
|
-
* Copy entries into the key-value table, verbatim: same key, same value,
|
|
53
|
-
* same absolute expiry. A live row holding a different value is kept unless
|
|
54
|
-
* `overwrite` is set, so a second copy of the same entries writes nothing.
|
|
55
|
-
* Writes go out in bounded transactions; the outcomes follow `entries`.
|
|
56
|
-
*/
|
|
57
|
-
export declare function copyIntoSql(driver: SqlDriver, entries: readonly SqlCopyEntry[], options: {
|
|
58
|
-
overwrite: boolean;
|
|
59
|
-
now: number;
|
|
60
|
-
}): Promise<SqlCopyOutcome[]>;
|
|
37
|
+
export {};
|
package/dist/storage/sql.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { runEdge } from "../runtime/run.js";
|
|
2
2
|
import { registerNativeStorage, registerNativeActivity } from "../runtime/native-storage.js";
|
|
3
|
+
import { observedWrite, observeStorageWrites } from "../runtime/request-reads.js";
|
|
3
4
|
import { nativeSqlDriver } from "./sql-driver.js";
|
|
4
|
-
import { nativeSqlStorage, nativeSqlActivityStore
|
|
5
|
-
/** The 0.28-compatible key-value table and expiry index. */
|
|
6
|
-
export { KV_SCHEMA } from "./sql-schema.js";
|
|
5
|
+
import { nativeSqlStorage, nativeSqlActivityStore } from "./sql-native.js";
|
|
7
6
|
/** `KVStorage` over one `connecta_kv` table. */
|
|
8
7
|
export function sqlStorage(driver, kind) {
|
|
9
|
-
const
|
|
10
|
-
|
|
8
|
+
const store = nativeSqlStorage(nativeSqlDriver(driver));
|
|
9
|
+
// Request read memos see every write begin and settle (request-reads.ts).
|
|
10
|
+
const root = {};
|
|
11
|
+
const native = {
|
|
12
|
+
...store,
|
|
13
|
+
set: (key, value, options) => observedWrite(root, key, store.set(key, value, options)),
|
|
14
|
+
delete: (key) => observedWrite(root, key, store.delete(key)),
|
|
15
|
+
compareAndSet: (key, expected, next, options) => observedWrite(root, key, store.compareAndSet(key, expected, next, options)),
|
|
16
|
+
};
|
|
17
|
+
const edge = registerNativeStorage({
|
|
11
18
|
capabilities: { absoluteExpiry: true },
|
|
12
19
|
describe: () => ({ kind }),
|
|
13
20
|
get: (key) => runEdge(native.get(key)),
|
|
@@ -16,6 +23,7 @@ export function sqlStorage(driver, kind) {
|
|
|
16
23
|
list: (prefix) => runEdge(native.list(prefix)),
|
|
17
24
|
compareAndSet: (key, expected, next, options) => runEdge(native.compareAndSet(key, expected, next, options)),
|
|
18
25
|
}, native);
|
|
26
|
+
return observeStorageWrites(edge, { root });
|
|
19
27
|
}
|
|
20
28
|
/**
|
|
21
29
|
* One append-only row per completed downstream call. Rows carry no arguments,
|
|
@@ -30,12 +38,3 @@ export function sqlActivityStore(driver, kind, options = {}) {
|
|
|
30
38
|
list: (options) => runEdge(native.list(options)),
|
|
31
39
|
}, native);
|
|
32
40
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Copy entries into the key-value table, verbatim: same key, same value,
|
|
35
|
-
* same absolute expiry. A live row holding a different value is kept unless
|
|
36
|
-
* `overwrite` is set, so a second copy of the same entries writes nothing.
|
|
37
|
-
* Writes go out in bounded transactions; the outcomes follow `entries`.
|
|
38
|
-
*/
|
|
39
|
-
export function copyIntoSql(driver, entries, options) {
|
|
40
|
-
return runEdge(copyIntoSqlEffect(nativeSqlDriver(driver), entries, options));
|
|
41
|
-
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Context } from "effect";
|
|
2
|
+
import { bytesToHex } from "./bytes.js";
|
|
3
|
+
import { serviceFor, setViewService } from "./runtime/call.js";
|
|
4
|
+
class StorageDigestKey extends Context.Service()("connecta/operation/StorageDigestKey") {
|
|
5
|
+
}
|
|
6
|
+
const importHmac = (raw) => crypto.subtle.importKey("raw", new Uint8Array(raw), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
7
|
+
// Drawn synchronously on first use, so concurrent first digests share one key.
|
|
8
|
+
let processBytes;
|
|
9
|
+
let processKey;
|
|
10
|
+
const fallbackKey = () => (processKey ??= importHmac((processBytes ??= crypto.getRandomValues(new Uint8Array(32)))));
|
|
11
|
+
const vaultKeys = new WeakMap();
|
|
12
|
+
/** Bind the vault's storage digest key to a registry-built context, when it has one. */
|
|
13
|
+
export function attachStorageDigestKey(ctx, vault) {
|
|
14
|
+
const derive = vault?.storageDigestKey;
|
|
15
|
+
if (typeof derive !== "function")
|
|
16
|
+
return;
|
|
17
|
+
let key = vaultKeys.get(vault);
|
|
18
|
+
if (!key) {
|
|
19
|
+
let pending;
|
|
20
|
+
key = () => (pending ??= derive.call(vault));
|
|
21
|
+
vaultKeys.set(vault, key);
|
|
22
|
+
}
|
|
23
|
+
setViewService(ctx, StorageDigestKey, key);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* `hmac:<hex>` of a secret-bearing value, for a stored key only. Stable for one
|
|
27
|
+
* deployment key, so equal inputs still address the same entry.
|
|
28
|
+
*/
|
|
29
|
+
export async function secretDigest(ctx, value) {
|
|
30
|
+
const key = await (serviceFor(ctx, StorageDigestKey) ?? fallbackKey)();
|
|
31
|
+
return `hmac:${bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(value))))}`;
|
|
32
|
+
}
|
package/dist/timeout.js
CHANGED
|
@@ -9,6 +9,14 @@ import { runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
|
9
9
|
* to trip only on a pathological hang, not a realistically slow probe.
|
|
10
10
|
*/
|
|
11
11
|
export const DEFAULT_PROBE_TIMEOUT_MS = CONFIG_DEFAULTS.discovery.probeTimeoutMs;
|
|
12
|
+
/**
|
|
13
|
+
* Each connector's probe deadline in an unscoped search, or
|
|
14
|
+
* `discovery.probeTimeoutMs` when that is shorter. Scoped search, describe
|
|
15
|
+
* and calls keep the full probe deadline.
|
|
16
|
+
*/
|
|
17
|
+
export const UNSCOPED_PROBE_TIMEOUT_MS = 5_000;
|
|
18
|
+
/** One unscoped search's whole discovery fan-out ends within this many ms. */
|
|
19
|
+
export const UNSCOPED_DISCOVERY_BUDGET_MS = 9_000;
|
|
12
20
|
/** A finite, positive integer number of milliseconds, or undefined. */
|
|
13
21
|
export function normalizeTimeoutMs(value) {
|
|
14
22
|
if (value === undefined || !Number.isFinite(value) || !(value > 0)) {
|