@zackbart/connecta 0.22.3 → 0.24.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/AGENTS.md +5 -0
- package/CHANGELOG.md +82 -0
- package/README.md +19 -11
- package/dist/activity-friction.d.ts +3 -0
- package/dist/activity-friction.js +19 -0
- package/dist/activity.d.ts +11 -2
- package/dist/activity.js +15 -19
- package/dist/auth/downstream-oauth.d.ts +2 -1
- package/dist/auth/downstream-oauth.js +10 -1
- package/dist/branding.d.ts +67 -0
- package/dist/branding.js +176 -0
- package/dist/catalog-service.d.ts +1 -6
- package/dist/catalog-service.js +3 -53
- package/dist/connectors/remote-mcp.js +3 -5
- package/dist/credential-contract.d.ts +24 -0
- package/dist/credential-contract.js +1 -0
- package/dist/credential-rules.d.ts +85 -0
- package/dist/credential-rules.js +107 -0
- package/dist/credentials.d.ts +4 -100
- package/dist/credentials.js +3 -107
- package/dist/errors.d.ts +1 -1
- package/dist/execute.d.ts +5 -52
- package/dist/execute.js +40 -347
- package/dist/executors/quickjs-protocol.d.ts +0 -7
- package/dist/executors/quickjs-protocol.js +2 -10
- package/dist/executors/quickjs.js +1 -1
- package/dist/index.d.ts +23 -56
- package/dist/index.js +30 -58
- package/dist/invocation.d.ts +0 -33
- package/dist/invocation.js +56 -124
- package/dist/meta-tools.d.ts +7 -6
- package/dist/meta-tools.js +14 -21
- package/dist/module-contracts.d.ts +19 -0
- package/dist/module-contracts.js +1 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +6 -3
- package/dist/operator-ui/view.d.ts +2 -18
- package/dist/operator-ui/view.js +3 -20
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +8 -6
- package/dist/routes/activity.js +1 -1
- package/dist/routes/credentials.js +5 -2
- package/dist/routes/mcp.js +7 -51
- package/dist/routes/oauth-management.d.ts +2 -0
- package/dist/routes/oauth-management.js +108 -0
- package/dist/routes/oauth.d.ts +0 -1
- package/dist/routes/oauth.js +21 -121
- package/dist/routes/shared.d.ts +19 -17
- package/dist/routes/shared.js +48 -44
- package/dist/routes/ui.js +36 -33
- package/dist/server.d.ts +1 -2
- package/dist/server.js +7 -45
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +55 -19
- package/dist/types.d.ts +3 -15
- package/dist/ui.d.ts +15 -70
- package/dist/ui.js +176 -317
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +31 -26
- package/documentation/auth.md +65 -111
- package/documentation/call-admission.md +10 -11
- package/documentation/cloudflare.md +1 -1
- package/documentation/code-mode.md +65 -260
- package/documentation/connectors.md +8 -6
- package/documentation/linear.md +1 -1
- package/documentation/meta-tools.md +37 -18
- package/documentation/mixpanel.md +1 -1
- package/documentation/notion.md +2 -2
- package/documentation/operations.md +19 -23
- package/documentation/operator-ui.md +82 -104
- package/documentation/optional-modules-upgrade.md +243 -0
- package/documentation/provider-conventions.md +7 -5
- package/documentation/revenuecat.md +1 -1
- package/documentation/storage-and-credentials.md +59 -40
- package/documentation/stripe.md +1 -1
- package/documentation/upgrading.md +83 -8
- package/ethos.md +36 -43
- package/examples/worker/AGENTS.md +3 -1
- package/examples/worker/README.md +68 -84
- package/examples/worker/src/d1-activity.ts +1 -1
- package/examples/worker/src/index.ts +11 -6
- package/package.json +18 -2
- package/templates/node/AGENTS.md +8 -6
- package/templates/node/README.md +56 -67
- package/templates/node/package.json +1 -1
- package/templates/node/src/file-activity.ts +1 -1
- package/templates/node/src/index.ts +11 -12
- package/dist/access-tokens.d.ts +0 -31
- package/dist/access-tokens.js +0 -236
- package/dist/apps-shell.d.ts +0 -37
- package/dist/apps-shell.js +0 -174
- package/dist/routes/access-tokens.d.ts +0 -6
- package/dist/routes/access-tokens.js +0 -83
package/dist/catalog-service.js
CHANGED
|
@@ -336,59 +336,6 @@ export class CatalogService {
|
|
|
336
336
|
catalogMs: Date.now() - started,
|
|
337
337
|
};
|
|
338
338
|
}
|
|
339
|
-
/**
|
|
340
|
-
* Resolve the JavaScript-safe property used by a lazy code-mode namespace
|
|
341
|
-
* back to exactly one catalog tool. Ambiguous aliases fail with an explicit
|
|
342
|
-
* escape hatch instead of silently choosing the first tool.
|
|
343
|
-
*/
|
|
344
|
-
async resolveToolAlias(connectorId, alias, aliasFor, callOptions = {}) {
|
|
345
|
-
const connector = this.registry.getConnector(connectorId);
|
|
346
|
-
if (!connector) {
|
|
347
|
-
return this.unknownAddressFailure(`${connectorId}.${alias}`, alias);
|
|
348
|
-
}
|
|
349
|
-
const started = Date.now();
|
|
350
|
-
let tools;
|
|
351
|
-
try {
|
|
352
|
-
tools = await this.loadConnector(connector.id, callOptions);
|
|
353
|
-
}
|
|
354
|
-
catch (cause) {
|
|
355
|
-
return this.catalogLoadFailure(cause, started, connector, alias);
|
|
356
|
-
}
|
|
357
|
-
const [definition, ...collisions] = tools.filter((tool) => aliasFor(tool.name) === alias);
|
|
358
|
-
if (!definition) {
|
|
359
|
-
return this.unknownToolFailure(alias, connector, started);
|
|
360
|
-
}
|
|
361
|
-
if (collisions.length > 0) {
|
|
362
|
-
const names = [definition, ...collisions]
|
|
363
|
-
.map((tool) => `"${tool.name}"`)
|
|
364
|
-
.join(", ");
|
|
365
|
-
return {
|
|
366
|
-
ok: false,
|
|
367
|
-
error: {
|
|
368
|
-
code: "ambiguous_tool_alias",
|
|
369
|
-
message: `Tool alias "${boundedEchoText(alias)}" is ambiguous on connector "${connector.id}" because ${names} sanitize to the same name. Use connecta.call with an exact address.`,
|
|
370
|
-
retryable: false,
|
|
371
|
-
nextAction: {
|
|
372
|
-
function: "connecta.call",
|
|
373
|
-
addresses: [definition, ...collisions].map((tool) => `${connector.id}.${tool.name}`),
|
|
374
|
-
purpose: "Choose the intended canonical address and call it with the original arguments.",
|
|
375
|
-
},
|
|
376
|
-
},
|
|
377
|
-
catalogMs: Date.now() - started,
|
|
378
|
-
connector,
|
|
379
|
-
toolName: alias,
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
return {
|
|
383
|
-
ok: true,
|
|
384
|
-
resolved: {
|
|
385
|
-
connector,
|
|
386
|
-
toolName: definition.name,
|
|
387
|
-
definition,
|
|
388
|
-
},
|
|
389
|
-
catalogMs: Date.now() - started,
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
339
|
async search(args) {
|
|
393
340
|
const query = args.query ?? "";
|
|
394
341
|
const retrievalQuery = lexicalSearchQuery(query);
|
|
@@ -894,6 +841,9 @@ export function flatSearchResult(page) {
|
|
|
894
841
|
return {
|
|
895
842
|
tools: page.entries.map((entry) => ({
|
|
896
843
|
...entry.tool,
|
|
844
|
+
...(entry.connector.title
|
|
845
|
+
? { connectorTitle: boundedEchoText(entry.connector.title.replace(/\s+/g, " ").trim(), 117) }
|
|
846
|
+
: {}),
|
|
897
847
|
...(entry.guide ? { guide: entry.guide } : {}),
|
|
898
848
|
...(entry.guideSummary
|
|
899
849
|
? { guideSummary: entry.guideSummary }
|
|
@@ -505,7 +505,7 @@ export function remoteMcp(id, opts) {
|
|
|
505
505
|
const newProvider = (ctx, state) => {
|
|
506
506
|
if (state?.provider)
|
|
507
507
|
return state.provider;
|
|
508
|
-
const provider = new KvOAuthProvider(id, ctx.storage, `${ctx.baseUrl}/oauth/callback/${id}`, refreshCoordinator);
|
|
508
|
+
const provider = new KvOAuthProvider(id, ctx.storage, `${ctx.baseUrl}/oauth/callback/${id}`, refreshCoordinator, ctx.allowAuthorization === true);
|
|
509
509
|
if (state)
|
|
510
510
|
state.provider = provider;
|
|
511
511
|
return provider;
|
|
@@ -1025,12 +1025,8 @@ export function remoteMcp(id, opts) {
|
|
|
1025
1025
|
// Only an OAuth connector has a pending consent URL to offer. A
|
|
1026
1026
|
// credential connector's downstream 401 is repaired on /credentials,
|
|
1027
1027
|
// so do not reach into OAuth storage to look for one.
|
|
1028
|
-
const url = isOauth
|
|
1029
|
-
? await newProvider(ctx, state).pendingAuthorizationUrl()
|
|
1030
|
-
: undefined;
|
|
1031
1028
|
return {
|
|
1032
1029
|
state: "auth_required",
|
|
1033
|
-
...(url !== undefined ? { authorizationUrl: url } : {}),
|
|
1034
1030
|
message: credentialAuth
|
|
1035
1031
|
? "Authorization required — the downstream rejected this connector's stored credential."
|
|
1036
1032
|
: "Authorization required — open the URL to connect.",
|
|
@@ -1070,7 +1066,9 @@ export function remoteMcp(id, opts) {
|
|
|
1070
1066
|
await disconnectAuthorization(ctx, stateFor(ctx), true);
|
|
1071
1067
|
};
|
|
1072
1068
|
connector.startAuth = async (ctx, startOpts) => {
|
|
1069
|
+
ctx = { ...ctx, requestScope: ctx.requestScope ?? ctx, allowAuthorization: true };
|
|
1073
1070
|
const state = stateFor(ctx);
|
|
1071
|
+
state.provider = null;
|
|
1074
1072
|
const p = newProvider(ctx, state);
|
|
1075
1073
|
if (startOpts?.force || (await p.operatorDisconnected())) {
|
|
1076
1074
|
await disconnectAuthorization(ctx, state);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ConnectorCredentialValues } from "./types.js";
|
|
2
|
+
interface CredentialFieldMetadata {
|
|
3
|
+
configured: true;
|
|
4
|
+
/** Only emitted when the value is long enough that four chars don't leak much. */
|
|
5
|
+
lastFour?: string;
|
|
6
|
+
updatedAt: string;
|
|
7
|
+
}
|
|
8
|
+
export interface CredentialMetadata {
|
|
9
|
+
configured: true;
|
|
10
|
+
/** Backward-compatible metadata for the reserved single credential field. */
|
|
11
|
+
lastFour?: string;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
/** Per-field masked metadata for named multi-value credentials. */
|
|
14
|
+
fields?: Record<string, CredentialFieldMetadata>;
|
|
15
|
+
}
|
|
16
|
+
export interface CredentialVault {
|
|
17
|
+
get(connectorId: string, field?: string, owner?: string): Promise<string | null>;
|
|
18
|
+
getAll(connectorId: string, owner?: string): Promise<ConnectorCredentialValues | null>;
|
|
19
|
+
metadata(connectorId: string, owner?: string): Promise<CredentialMetadata | null>;
|
|
20
|
+
set(connectorId: string, value: string, updatedBy: string, owner?: string): Promise<CredentialMetadata>;
|
|
21
|
+
setAll(connectorId: string, values: ConnectorCredentialValues, updatedBy: string, owner?: string): Promise<CredentialMetadata>;
|
|
22
|
+
delete(connectorId: string, owner?: string): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Connector, ConnectorCredentialConfig } from "./types.js";
|
|
2
|
+
/** Which hook a testable credential is checked with. */
|
|
3
|
+
type CredentialTestMode = "single" | "multiple";
|
|
4
|
+
/** Operator-safe explanation shared by every surface that detects shape drift. */
|
|
5
|
+
export declare const STORED_CREDENTIAL_SHAPE_MISMATCH_ERROR = "Stored credential fields do not match this connector's current declaration. Replace the credential before using or testing this connector.";
|
|
6
|
+
export type StoredCredentialShape = {
|
|
7
|
+
state: "missing";
|
|
8
|
+
} | {
|
|
9
|
+
state: "valid";
|
|
10
|
+
mode: CredentialTestMode;
|
|
11
|
+
/**
|
|
12
|
+
* Stored keys the connector no longer declares, sorted. Harmless — the
|
|
13
|
+
* credential works — but worth telling an operator about, since nothing
|
|
14
|
+
* else in `/ui` can show a field the declaration has stopped naming.
|
|
15
|
+
*/
|
|
16
|
+
undeclared: string[];
|
|
17
|
+
} | {
|
|
18
|
+
state: "mismatch";
|
|
19
|
+
mode: CredentialTestMode;
|
|
20
|
+
message: typeof STORED_CREDENTIAL_SHAPE_MISMATCH_ERROR;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Compare a connector's current declaration with the keys in its stored
|
|
24
|
+
* credential. Values are deliberately ignored: callers may pass decrypted
|
|
25
|
+
* values or `/credentials`' masked field metadata and get the same answer.
|
|
26
|
+
*
|
|
27
|
+
* The test is CONTAINMENT, not equality: the stored key set is compatible when
|
|
28
|
+
* it holds every field currently declared — the reserved `value` key for a
|
|
29
|
+
* single-value declaration, every declared name for a named one. Anything the
|
|
30
|
+
* declaration asks for and the vault does not have is `mismatch`, which is
|
|
31
|
+
* precisely what a renamed field, a newly added field, or a swap between the
|
|
32
|
+
* two shapes produces. The swap needs no special case: `value` is never one of
|
|
33
|
+
* the declared names, so single→named and named→single each leave the declared
|
|
34
|
+
* side unsatisfied. An empty stored map (reachable through a hand-written
|
|
35
|
+
* plaintext) satisfies nothing and is a mismatch too.
|
|
36
|
+
*
|
|
37
|
+
* Extra keys are NOT drift. They are what *dropping* a field leaves behind, and
|
|
38
|
+
* every accessor a connector actually uses — `ctx.credential.get("apiKey")`,
|
|
39
|
+
* `getAll().apiKey` — keeps returning the right secret across that redeploy.
|
|
40
|
+
* Calling it drift would order an operator to re-enter a working secret that
|
|
41
|
+
* many providers will not reissue in readable form. The leftovers come back as
|
|
42
|
+
* `undeclared` instead, for a surface to mention without blocking anything.
|
|
43
|
+
*/
|
|
44
|
+
export declare function storedCredentialShape(config: ConnectorCredentialConfig, stored: Readonly<Record<string, unknown>> | null): StoredCredentialShape;
|
|
45
|
+
/**
|
|
46
|
+
* The one sentence describing leftover stored fields, so every surface words it
|
|
47
|
+
* the same way. Names only — the values stay in the vault, and a field name from
|
|
48
|
+
* a previous declaration is not a secret. Deliberately reassuring: nothing is
|
|
49
|
+
* broken, and the only thing an operator gains by acting is that a connector
|
|
50
|
+
* iterating `getAll()` stops seeing a field its code no longer knows about.
|
|
51
|
+
*/
|
|
52
|
+
export declare function describeUndeclaredCredentialFields(fields: string[]): string;
|
|
53
|
+
/** A declared credential shape whose only test hook cannot test it. */
|
|
54
|
+
export interface CredentialTestMismatch {
|
|
55
|
+
/** The shape the connector declared. */
|
|
56
|
+
shape: CredentialTestMode;
|
|
57
|
+
/** The hook it implements, which that shape cannot use. */
|
|
58
|
+
hook: "testCredential" | "testCredentials";
|
|
59
|
+
}
|
|
60
|
+
export interface CredentialTestRule {
|
|
61
|
+
/** The hook to call, or null when this credential cannot be tested at all. */
|
|
62
|
+
mode: CredentialTestMode | null;
|
|
63
|
+
/** Set only when the sole implemented hook is the one the shape cannot use. */
|
|
64
|
+
mismatch?: CredentialTestMismatch;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The one rule deciding whether a connector's credential can be tested — read
|
|
68
|
+
* by /credentials' `testable` flag, by `POST /ui/credentials/<id>/test` when
|
|
69
|
+
* it picks a hook, and by the construction-time mismatch warning, so those
|
|
70
|
+
* three cannot drift apart.
|
|
71
|
+
*
|
|
72
|
+
* The declared credential *shape* selects the hook: named `credential.fields`
|
|
73
|
+
* are tested as a set by `testCredentials`, a single-value `credential` by
|
|
74
|
+
* `testCredential` on the vault's reserved `value` field. The other hook is
|
|
75
|
+
* never substituted — it would be handed a shape the connector never declared —
|
|
76
|
+
* so a connector implementing only the mismatched hook is not testable, and
|
|
77
|
+
* says so at construction rather than under an operator's click.
|
|
78
|
+
*/
|
|
79
|
+
export declare function credentialTestRule(connector: Pick<Connector, "credential" | "testCredential" | "testCredentials">): CredentialTestRule;
|
|
80
|
+
/**
|
|
81
|
+
* One clause naming a mismatch, shared by the startup warning and the test
|
|
82
|
+
* route's 400 so an operator reads the same explanation in both places.
|
|
83
|
+
*/
|
|
84
|
+
export declare function describeCredentialTestMismatch(mismatch: CredentialTestMismatch): string;
|
|
85
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** Operator-safe explanation shared by every surface that detects shape drift. */
|
|
2
|
+
export const STORED_CREDENTIAL_SHAPE_MISMATCH_ERROR = "Stored credential fields do not match this connector's current declaration. Replace the credential before using or testing this connector.";
|
|
3
|
+
/**
|
|
4
|
+
* Compare a connector's current declaration with the keys in its stored
|
|
5
|
+
* credential. Values are deliberately ignored: callers may pass decrypted
|
|
6
|
+
* values or `/credentials`' masked field metadata and get the same answer.
|
|
7
|
+
*
|
|
8
|
+
* The test is CONTAINMENT, not equality: the stored key set is compatible when
|
|
9
|
+
* it holds every field currently declared — the reserved `value` key for a
|
|
10
|
+
* single-value declaration, every declared name for a named one. Anything the
|
|
11
|
+
* declaration asks for and the vault does not have is `mismatch`, which is
|
|
12
|
+
* precisely what a renamed field, a newly added field, or a swap between the
|
|
13
|
+
* two shapes produces. The swap needs no special case: `value` is never one of
|
|
14
|
+
* the declared names, so single→named and named→single each leave the declared
|
|
15
|
+
* side unsatisfied. An empty stored map (reachable through a hand-written
|
|
16
|
+
* plaintext) satisfies nothing and is a mismatch too.
|
|
17
|
+
*
|
|
18
|
+
* Extra keys are NOT drift. They are what *dropping* a field leaves behind, and
|
|
19
|
+
* every accessor a connector actually uses — `ctx.credential.get("apiKey")`,
|
|
20
|
+
* `getAll().apiKey` — keeps returning the right secret across that redeploy.
|
|
21
|
+
* Calling it drift would order an operator to re-enter a working secret that
|
|
22
|
+
* many providers will not reissue in readable form. The leftovers come back as
|
|
23
|
+
* `undeclared` instead, for a surface to mention without blocking anything.
|
|
24
|
+
*/
|
|
25
|
+
export function storedCredentialShape(config, stored) {
|
|
26
|
+
if (!stored)
|
|
27
|
+
return { state: "missing" };
|
|
28
|
+
const mode = config.fields?.length
|
|
29
|
+
? "multiple"
|
|
30
|
+
: "single";
|
|
31
|
+
const declared = new Set(mode === "multiple" ? config.fields.map((field) => field.name) : ["value"]);
|
|
32
|
+
const actual = Object.keys(stored);
|
|
33
|
+
const present = new Set(actual);
|
|
34
|
+
for (const field of declared) {
|
|
35
|
+
if (!present.has(field)) {
|
|
36
|
+
return {
|
|
37
|
+
state: "mismatch",
|
|
38
|
+
mode,
|
|
39
|
+
message: STORED_CREDENTIAL_SHAPE_MISMATCH_ERROR,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
state: "valid",
|
|
45
|
+
mode,
|
|
46
|
+
undeclared: actual.filter((field) => !declared.has(field)).sort(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** How many leftover field names an advisory names before it summarizes. */
|
|
50
|
+
const UNDECLARED_SAMPLE = 5;
|
|
51
|
+
/**
|
|
52
|
+
* The one sentence describing leftover stored fields, so every surface words it
|
|
53
|
+
* the same way. Names only — the values stay in the vault, and a field name from
|
|
54
|
+
* a previous declaration is not a secret. Deliberately reassuring: nothing is
|
|
55
|
+
* broken, and the only thing an operator gains by acting is that a connector
|
|
56
|
+
* iterating `getAll()` stops seeing a field its code no longer knows about.
|
|
57
|
+
*/
|
|
58
|
+
export function describeUndeclaredCredentialFields(fields) {
|
|
59
|
+
const shown = fields.slice(0, UNDECLARED_SAMPLE);
|
|
60
|
+
const rest = fields.length - shown.length;
|
|
61
|
+
const named = shown.join(", ") + (rest > 0 ? `, and ${rest} more` : "");
|
|
62
|
+
return fields.length === 1
|
|
63
|
+
? `Stored credential also holds a field this connector no longer declares (${named}). It keeps working; replace the credential to drop it.`
|
|
64
|
+
: `Stored credential also holds fields this connector no longer declares (${named}). It keeps working; replace the credential to drop them.`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The one rule deciding whether a connector's credential can be tested — read
|
|
68
|
+
* by /credentials' `testable` flag, by `POST /ui/credentials/<id>/test` when
|
|
69
|
+
* it picks a hook, and by the construction-time mismatch warning, so those
|
|
70
|
+
* three cannot drift apart.
|
|
71
|
+
*
|
|
72
|
+
* The declared credential *shape* selects the hook: named `credential.fields`
|
|
73
|
+
* are tested as a set by `testCredentials`, a single-value `credential` by
|
|
74
|
+
* `testCredential` on the vault's reserved `value` field. The other hook is
|
|
75
|
+
* never substituted — it would be handed a shape the connector never declared —
|
|
76
|
+
* so a connector implementing only the mismatched hook is not testable, and
|
|
77
|
+
* says so at construction rather than under an operator's click.
|
|
78
|
+
*/
|
|
79
|
+
export function credentialTestRule(connector) {
|
|
80
|
+
if (!connector.credential)
|
|
81
|
+
return { mode: null };
|
|
82
|
+
if (connector.credential.fields?.length) {
|
|
83
|
+
if (connector.testCredentials)
|
|
84
|
+
return { mode: "multiple" };
|
|
85
|
+
return connector.testCredential
|
|
86
|
+
? { mode: null, mismatch: { shape: "multiple", hook: "testCredential" } }
|
|
87
|
+
: { mode: null };
|
|
88
|
+
}
|
|
89
|
+
if (connector.testCredential)
|
|
90
|
+
return { mode: "single" };
|
|
91
|
+
return connector.testCredentials
|
|
92
|
+
? { mode: null, mismatch: { shape: "single", hook: "testCredentials" } }
|
|
93
|
+
: { mode: null };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* One clause naming a mismatch, shared by the startup warning and the test
|
|
97
|
+
* route's 400 so an operator reads the same explanation in both places.
|
|
98
|
+
*/
|
|
99
|
+
export function describeCredentialTestMismatch(mismatch) {
|
|
100
|
+
return mismatch.shape === "multiple"
|
|
101
|
+
? "it declares named credential fields, which only " +
|
|
102
|
+
"`testCredentials(values, ctx)` can test, but implements " +
|
|
103
|
+
"`testCredential`"
|
|
104
|
+
: "it declares a single-value credential, which only " +
|
|
105
|
+
"`testCredential(value, ctx)` can test, but implements " +
|
|
106
|
+
"`testCredentials`";
|
|
107
|
+
}
|
package/dist/credentials.d.ts
CHANGED
|
@@ -1,107 +1,11 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
configured: true;
|
|
4
|
-
/** Only emitted when the value is long enough that four chars don't leak much. */
|
|
5
|
-
lastFour?: string;
|
|
6
|
-
updatedAt: string;
|
|
7
|
-
}
|
|
8
|
-
export interface CredentialMetadata {
|
|
9
|
-
configured: true;
|
|
10
|
-
/** Backward-compatible metadata for the reserved single credential field. */
|
|
11
|
-
lastFour?: string;
|
|
12
|
-
updatedAt: string;
|
|
13
|
-
/** Per-field masked metadata for named multi-value credentials. */
|
|
14
|
-
fields?: Record<string, CredentialFieldMetadata>;
|
|
15
|
-
}
|
|
16
|
-
/** Which hook a testable credential is checked with. */
|
|
17
|
-
type CredentialTestMode = "single" | "multiple";
|
|
18
|
-
/** Operator-safe explanation shared by every surface that detects shape drift. */
|
|
19
|
-
export declare const STORED_CREDENTIAL_SHAPE_MISMATCH_ERROR = "Stored credential fields do not match this connector's current declaration. Replace the credential before using or testing this connector.";
|
|
20
|
-
export type StoredCredentialShape = {
|
|
21
|
-
state: "missing";
|
|
22
|
-
} | {
|
|
23
|
-
state: "valid";
|
|
24
|
-
mode: CredentialTestMode;
|
|
25
|
-
/**
|
|
26
|
-
* Stored keys the connector no longer declares, sorted. Harmless — the
|
|
27
|
-
* credential works — but worth telling an operator about, since nothing
|
|
28
|
-
* else in `/ui` can show a field the declaration has stopped naming.
|
|
29
|
-
*/
|
|
30
|
-
undeclared: string[];
|
|
31
|
-
} | {
|
|
32
|
-
state: "mismatch";
|
|
33
|
-
mode: CredentialTestMode;
|
|
34
|
-
message: typeof STORED_CREDENTIAL_SHAPE_MISMATCH_ERROR;
|
|
35
|
-
};
|
|
36
|
-
/**
|
|
37
|
-
* Compare a connector's current declaration with the keys in its stored
|
|
38
|
-
* credential. Values are deliberately ignored: callers may pass decrypted
|
|
39
|
-
* values or `/credentials`' masked field metadata and get the same answer.
|
|
40
|
-
*
|
|
41
|
-
* The test is CONTAINMENT, not equality: the stored key set is compatible when
|
|
42
|
-
* it holds every field currently declared — the reserved `value` key for a
|
|
43
|
-
* single-value declaration, every declared name for a named one. Anything the
|
|
44
|
-
* declaration asks for and the vault does not have is `mismatch`, which is
|
|
45
|
-
* precisely what a renamed field, a newly added field, or a swap between the
|
|
46
|
-
* two shapes produces. The swap needs no special case: `value` is never one of
|
|
47
|
-
* the declared names, so single→named and named→single each leave the declared
|
|
48
|
-
* side unsatisfied. An empty stored map (reachable through a hand-written
|
|
49
|
-
* plaintext) satisfies nothing and is a mismatch too.
|
|
50
|
-
*
|
|
51
|
-
* Extra keys are NOT drift. They are what *dropping* a field leaves behind, and
|
|
52
|
-
* every accessor a connector actually uses — `ctx.credential.get("apiKey")`,
|
|
53
|
-
* `getAll().apiKey` — keeps returning the right secret across that redeploy.
|
|
54
|
-
* Calling it drift would order an operator to re-enter a working secret that
|
|
55
|
-
* many providers will not reissue in readable form. The leftovers come back as
|
|
56
|
-
* `undeclared` instead, for a surface to mention without blocking anything.
|
|
57
|
-
*/
|
|
58
|
-
export declare function storedCredentialShape(config: ConnectorCredentialConfig, stored: Readonly<Record<string, unknown>> | null): StoredCredentialShape;
|
|
59
|
-
/**
|
|
60
|
-
* The one sentence describing leftover stored fields, so every surface words it
|
|
61
|
-
* the same way. Names only — the values stay in the vault, and a field name from
|
|
62
|
-
* a previous declaration is not a secret. Deliberately reassuring: nothing is
|
|
63
|
-
* broken, and the only thing an operator gains by acting is that a connector
|
|
64
|
-
* iterating `getAll()` stops seeing a field its code no longer knows about.
|
|
65
|
-
*/
|
|
66
|
-
export declare function describeUndeclaredCredentialFields(fields: string[]): string;
|
|
67
|
-
/** A declared credential shape whose only test hook cannot test it. */
|
|
68
|
-
export interface CredentialTestMismatch {
|
|
69
|
-
/** The shape the connector declared. */
|
|
70
|
-
shape: CredentialTestMode;
|
|
71
|
-
/** The hook it implements, which that shape cannot use. */
|
|
72
|
-
hook: "testCredential" | "testCredentials";
|
|
73
|
-
}
|
|
74
|
-
export interface CredentialTestRule {
|
|
75
|
-
/** The hook to call, or null when this credential cannot be tested at all. */
|
|
76
|
-
mode: CredentialTestMode | null;
|
|
77
|
-
/** Set only when the sole implemented hook is the one the shape cannot use. */
|
|
78
|
-
mismatch?: CredentialTestMismatch;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* The one rule deciding whether a connector's credential can be tested — read
|
|
82
|
-
* by /credentials' `testable` flag, by `POST /ui/credentials/<id>/test` when
|
|
83
|
-
* it picks a hook, and by the construction-time mismatch warning, so those
|
|
84
|
-
* three cannot drift apart.
|
|
85
|
-
*
|
|
86
|
-
* The declared credential *shape* selects the hook: named `credential.fields`
|
|
87
|
-
* are tested as a set by `testCredentials`, a single-value `credential` by
|
|
88
|
-
* `testCredential` on the vault's reserved `value` field. The other hook is
|
|
89
|
-
* never substituted — it would be handed a shape the connector never declared —
|
|
90
|
-
* so a connector implementing only the mismatched hook is not testable, and
|
|
91
|
-
* says so at construction rather than under an operator's click.
|
|
92
|
-
*/
|
|
93
|
-
export declare function credentialTestRule(connector: Pick<Connector, "credential" | "testCredential" | "testCredentials">): CredentialTestRule;
|
|
94
|
-
/**
|
|
95
|
-
* One clause naming a mismatch, shared by the startup warning and the test
|
|
96
|
-
* route's 400 so an operator reads the same explanation in both places.
|
|
97
|
-
*/
|
|
98
|
-
export declare function describeCredentialTestMismatch(mismatch: CredentialTestMismatch): string;
|
|
1
|
+
import type { CredentialMetadata, CredentialVault as Vault } from "./credential-contract.js";
|
|
2
|
+
import type { ConnectorCredentialValues, KVStorage } from "./types.js";
|
|
99
3
|
/**
|
|
100
4
|
* Encrypted, connector-scoped credential vault over the deployment's existing
|
|
101
5
|
* KVStorage. Only ciphertext enters KV; the AES-GCM key remains an environment
|
|
102
6
|
* secret outside the store.
|
|
103
7
|
*/
|
|
104
|
-
export declare class CredentialVault {
|
|
8
|
+
export declare class CredentialVault implements Vault {
|
|
105
9
|
private readonly storage;
|
|
106
10
|
private readonly key;
|
|
107
11
|
constructor(storage: KVStorage, encryptionKey: string);
|
|
@@ -114,4 +18,4 @@ export declare class CredentialVault {
|
|
|
114
18
|
setAll(connectorId: string, values: ConnectorCredentialValues, updatedBy: string, owner?: string): Promise<CredentialMetadata>;
|
|
115
19
|
delete(connectorId: string, owner?: string): Promise<void>;
|
|
116
20
|
}
|
|
117
|
-
export
|
|
21
|
+
export declare function encryptedCredentialVault(storage: KVStorage, encryptionKey: string): Vault;
|
package/dist/credentials.js
CHANGED
|
@@ -3,113 +3,6 @@ const IV_BYTES = 12;
|
|
|
3
3
|
const MAX_CREDENTIAL_BYTES = 16_384;
|
|
4
4
|
const encoder = new TextEncoder();
|
|
5
5
|
const decoder = new TextDecoder();
|
|
6
|
-
/** Operator-safe explanation shared by every surface that detects shape drift. */
|
|
7
|
-
export const STORED_CREDENTIAL_SHAPE_MISMATCH_ERROR = "Stored credential fields do not match this connector's current declaration. Replace the credential before using or testing this connector.";
|
|
8
|
-
/**
|
|
9
|
-
* Compare a connector's current declaration with the keys in its stored
|
|
10
|
-
* credential. Values are deliberately ignored: callers may pass decrypted
|
|
11
|
-
* values or `/credentials`' masked field metadata and get the same answer.
|
|
12
|
-
*
|
|
13
|
-
* The test is CONTAINMENT, not equality: the stored key set is compatible when
|
|
14
|
-
* it holds every field currently declared — the reserved `value` key for a
|
|
15
|
-
* single-value declaration, every declared name for a named one. Anything the
|
|
16
|
-
* declaration asks for and the vault does not have is `mismatch`, which is
|
|
17
|
-
* precisely what a renamed field, a newly added field, or a swap between the
|
|
18
|
-
* two shapes produces. The swap needs no special case: `value` is never one of
|
|
19
|
-
* the declared names, so single→named and named→single each leave the declared
|
|
20
|
-
* side unsatisfied. An empty stored map (reachable through a hand-written
|
|
21
|
-
* plaintext) satisfies nothing and is a mismatch too.
|
|
22
|
-
*
|
|
23
|
-
* Extra keys are NOT drift. They are what *dropping* a field leaves behind, and
|
|
24
|
-
* every accessor a connector actually uses — `ctx.credential.get("apiKey")`,
|
|
25
|
-
* `getAll().apiKey` — keeps returning the right secret across that redeploy.
|
|
26
|
-
* Calling it drift would order an operator to re-enter a working secret that
|
|
27
|
-
* many providers will not reissue in readable form. The leftovers come back as
|
|
28
|
-
* `undeclared` instead, for a surface to mention without blocking anything.
|
|
29
|
-
*/
|
|
30
|
-
export function storedCredentialShape(config, stored) {
|
|
31
|
-
if (!stored)
|
|
32
|
-
return { state: "missing" };
|
|
33
|
-
const mode = config.fields?.length
|
|
34
|
-
? "multiple"
|
|
35
|
-
: "single";
|
|
36
|
-
const declared = new Set(mode === "multiple" ? config.fields.map((field) => field.name) : ["value"]);
|
|
37
|
-
const actual = Object.keys(stored);
|
|
38
|
-
const present = new Set(actual);
|
|
39
|
-
for (const field of declared) {
|
|
40
|
-
if (!present.has(field)) {
|
|
41
|
-
return {
|
|
42
|
-
state: "mismatch",
|
|
43
|
-
mode,
|
|
44
|
-
message: STORED_CREDENTIAL_SHAPE_MISMATCH_ERROR,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return {
|
|
49
|
-
state: "valid",
|
|
50
|
-
mode,
|
|
51
|
-
undeclared: actual.filter((field) => !declared.has(field)).sort(),
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
/** How many leftover field names an advisory names before it summarizes. */
|
|
55
|
-
const UNDECLARED_SAMPLE = 5;
|
|
56
|
-
/**
|
|
57
|
-
* The one sentence describing leftover stored fields, so every surface words it
|
|
58
|
-
* the same way. Names only — the values stay in the vault, and a field name from
|
|
59
|
-
* a previous declaration is not a secret. Deliberately reassuring: nothing is
|
|
60
|
-
* broken, and the only thing an operator gains by acting is that a connector
|
|
61
|
-
* iterating `getAll()` stops seeing a field its code no longer knows about.
|
|
62
|
-
*/
|
|
63
|
-
export function describeUndeclaredCredentialFields(fields) {
|
|
64
|
-
const shown = fields.slice(0, UNDECLARED_SAMPLE);
|
|
65
|
-
const rest = fields.length - shown.length;
|
|
66
|
-
const named = shown.join(", ") + (rest > 0 ? `, and ${rest} more` : "");
|
|
67
|
-
return fields.length === 1
|
|
68
|
-
? `Stored credential also holds a field this connector no longer declares (${named}). It keeps working; replace the credential to drop it.`
|
|
69
|
-
: `Stored credential also holds fields this connector no longer declares (${named}). It keeps working; replace the credential to drop them.`;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* The one rule deciding whether a connector's credential can be tested — read
|
|
73
|
-
* by /credentials' `testable` flag, by `POST /ui/credentials/<id>/test` when
|
|
74
|
-
* it picks a hook, and by the construction-time mismatch warning, so those
|
|
75
|
-
* three cannot drift apart.
|
|
76
|
-
*
|
|
77
|
-
* The declared credential *shape* selects the hook: named `credential.fields`
|
|
78
|
-
* are tested as a set by `testCredentials`, a single-value `credential` by
|
|
79
|
-
* `testCredential` on the vault's reserved `value` field. The other hook is
|
|
80
|
-
* never substituted — it would be handed a shape the connector never declared —
|
|
81
|
-
* so a connector implementing only the mismatched hook is not testable, and
|
|
82
|
-
* says so at construction rather than under an operator's click.
|
|
83
|
-
*/
|
|
84
|
-
export function credentialTestRule(connector) {
|
|
85
|
-
if (!connector.credential)
|
|
86
|
-
return { mode: null };
|
|
87
|
-
if (connector.credential.fields?.length) {
|
|
88
|
-
if (connector.testCredentials)
|
|
89
|
-
return { mode: "multiple" };
|
|
90
|
-
return connector.testCredential
|
|
91
|
-
? { mode: null, mismatch: { shape: "multiple", hook: "testCredential" } }
|
|
92
|
-
: { mode: null };
|
|
93
|
-
}
|
|
94
|
-
if (connector.testCredential)
|
|
95
|
-
return { mode: "single" };
|
|
96
|
-
return connector.testCredentials
|
|
97
|
-
? { mode: null, mismatch: { shape: "single", hook: "testCredentials" } }
|
|
98
|
-
: { mode: null };
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* One clause naming a mismatch, shared by the startup warning and the test
|
|
102
|
-
* route's 400 so an operator reads the same explanation in both places.
|
|
103
|
-
*/
|
|
104
|
-
export function describeCredentialTestMismatch(mismatch) {
|
|
105
|
-
return mismatch.shape === "multiple"
|
|
106
|
-
? "it declares named credential fields, which only " +
|
|
107
|
-
"`testCredentials(values, ctx)` can test, but implements " +
|
|
108
|
-
"`testCredential`"
|
|
109
|
-
: "it declares a single-value credential, which only " +
|
|
110
|
-
"`testCredential(value, ctx)` can test, but implements " +
|
|
111
|
-
"`testCredentials`";
|
|
112
|
-
}
|
|
113
6
|
function storageKey(connectorId, owner) {
|
|
114
7
|
return owner
|
|
115
8
|
? `principal:${owner}:conn:${connectorId}:credential:v1`
|
|
@@ -290,3 +183,6 @@ export class CredentialVault {
|
|
|
290
183
|
await this.storage.delete(storageKey(connectorId, owner));
|
|
291
184
|
}
|
|
292
185
|
}
|
|
186
|
+
export function encryptedCredentialVault(storage, encryptionKey) {
|
|
187
|
+
return new CredentialVault(storage, encryptionKey);
|
|
188
|
+
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -74,7 +74,7 @@ export declare class ConnectorCallError extends Error {
|
|
|
74
74
|
validation?: ArgumentValidationDetails;
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
|
-
/** The `error` object surfaced in value-mode call results and
|
|
77
|
+
/** The `error` object surfaced in value-mode call results and rejected promises. */
|
|
78
78
|
export interface CallErrorDetails {
|
|
79
79
|
code: string;
|
|
80
80
|
message: string;
|