@zackbart/connecta 0.32.0 → 0.34.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 +98 -0
- package/README.md +95 -116
- package/SECURITY.md +1 -1
- package/bin/connecta.mjs +24 -6
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +167 -23
- package/dist/access-tokens.js +676 -140
- package/dist/activity.d.ts +3 -0
- package/dist/agent-identity.js +47 -0
- package/dist/auth/cloudflare-access.js +13 -5
- package/dist/branding.d.ts +7 -0
- package/dist/branding.js +76 -9
- package/dist/call-admission-context.js +15 -0
- package/dist/call-admission.js +30 -43
- package/dist/call-recovery.js +6 -2
- package/dist/catalog-service.js +15 -1
- package/dist/config-value-sources.js +16 -9
- package/dist/config.d.ts +48 -75
- package/dist/config.js +144 -36
- package/dist/connector-access.d.ts +4 -34
- package/dist/connector-access.js +15 -99
- package/dist/connector-caller.d.ts +7 -1
- package/dist/connector-caller.js +4 -2
- package/dist/connectors/api-connector.d.ts +27 -4
- package/dist/connectors/api-connector.js +2 -0
- package/dist/connectors/remote-mcp.js +2 -0
- package/dist/describe-config.d.ts +31 -7
- package/dist/describe-config.js +8 -17
- package/dist/execute.js +1 -11
- package/dist/fix-prompt.js +2 -2
- package/dist/index.d.ts +4 -3
- package/dist/index.js +30 -10
- package/dist/invocation.js +5 -3
- package/dist/membership.d.ts +53 -0
- package/dist/membership.js +121 -0
- package/dist/module-contracts.d.ts +23 -6
- package/dist/oauth-callback-outcome.js +2 -2
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -4
- package/dist/operator-ui/agents.d.ts +74 -0
- package/dist/operator-ui/agents.js +104 -0
- package/dist/operator-ui/config-summary.js +562 -0
- package/dist/operator-ui/connection-map.js +375 -0
- package/dist/operator-ui/contract.d.ts +149 -28
- package/dist/operator-ui/endpoints.js +221 -0
- package/dist/operator-ui/fix-prompts.js +5 -5
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/member-page.js +97 -0
- package/dist/operator-ui/model.d.ts +14 -6
- package/dist/operator-ui/overview.js +134 -0
- package/dist/operator-ui/view.d.ts +107 -37
- package/dist/operator-ui/view.js +163 -86
- package/dist/page-styles.js +5 -3
- package/dist/provider.d.ts +61 -8
- package/dist/provider.js +46 -7
- package/dist/providers/_shared/google/workspace.d.ts +56 -17
- package/dist/providers/_shared/google/workspace.js +34 -8
- package/dist/providers/_shared/rest/tools.js +2 -2
- package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
- package/dist/providers/_shared/rest/value-safety.js +699 -7
- package/dist/providers/ccb/index.js +2 -2
- package/dist/providers/cloudflare/index.js +3 -3
- package/dist/providers/cloudflare/rest.js +25 -3
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/cloudflare/value-safety.js +32 -0
- package/dist/providers/docs/index.js +4 -4
- package/dist/providers/docs/skill.generated.js +2 -2
- package/dist/providers/drive/index.js +4 -4
- package/dist/providers/drive/skill.generated.js +2 -2
- package/dist/providers/forms/index.js +4 -4
- package/dist/providers/forms/skill.generated.js +2 -2
- package/dist/providers/gmail/index.js +11 -9
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/mixpanel/index.js +1 -1
- package/dist/providers/mixpanel/skill.generated.js +3 -3
- package/dist/providers/notion/index.d.ts +7 -0
- package/dist/providers/notion/index.js +100 -11
- package/dist/providers/notion/rest.js +45 -5
- package/dist/providers/notion/skill.generated.js +2 -2
- package/dist/providers/notion/value-safety.js +30 -5
- package/dist/providers/overflow/index.js +2 -2
- package/dist/providers/overflow/skill.generated.js +2 -2
- package/dist/providers/planning-center/index.js +2 -2
- package/dist/providers/planning-center/skill.generated.js +4 -4
- package/dist/providers/revenuecat/skill.generated.js +2 -2
- package/dist/providers/sheets/index.d.ts +3 -0
- package/dist/providers/sheets/index.js +7 -4
- package/dist/providers/sheets/skill.generated.js +2 -2
- package/dist/providers/slides/index.js +4 -4
- package/dist/providers/slides/skill.generated.js +2 -2
- package/dist/providers/stripe/rest.js +9 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/stripe/value-safety.js +22 -0
- package/dist/providers/tithely/index.js +1 -1
- package/dist/providers/vercel/index.js +1 -1
- package/dist/providers/vercel/rest.js +20 -2
- package/dist/providers/vercel/skill.generated.js +2 -2
- package/dist/providers/vercel/value-safety.js +22 -0
- package/dist/registry.d.ts +2 -3
- package/dist/registry.js +23 -9
- package/dist/routes/access-tokens.js +82 -75
- package/dist/routes/activity-disclosure.js +17 -18
- package/dist/routes/activity.js +34 -54
- package/dist/routes/actor-labels.js +33 -4
- package/dist/routes/agent-path.js +43 -0
- package/dist/routes/connect.js +16 -12
- package/dist/routes/credentials.js +16 -40
- package/dist/routes/mcp.js +161 -104
- package/dist/routes/oauth-management.js +11 -5
- package/dist/routes/oauth.js +5 -11
- package/dist/routes/operator.js +24 -23
- package/dist/routes/shared.d.ts +60 -20
- package/dist/routes/shared.js +187 -96
- package/dist/routes/ui-config.js +437 -97
- package/dist/routes/ui-data.js +2 -2
- package/dist/routes/ui.js +9 -16
- package/dist/runtime/call-admission.js +9 -0
- package/dist/server.js +5 -0
- package/dist/skills.js +10 -0
- package/dist/storage/keys.js +9 -1
- package/dist/storage/sql.js +5 -1
- package/dist/types.d.ts +29 -7
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +23 -16
- package/dist/usage-guide.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace-address.js +9 -0
- package/dist/workspace-subject.js +23 -0
- package/documentation/README.md +47 -23
- package/documentation/architecture.md +371 -61
- package/documentation/auth.md +897 -249
- package/documentation/code-mode.md +37 -16
- package/documentation/deploying.md +126 -25
- package/documentation/integrating.md +97 -23
- package/documentation/meta-tools.md +50 -31
- package/documentation/migration-0.33.md +352 -0
- package/documentation/migration-0.34.md +67 -0
- package/documentation/operating.md +3 -1
- package/documentation/operator-tests.md +147 -34
- package/documentation/operator-ui.md +519 -64
- package/documentation/provider-auth-migration.md +37 -23
- package/documentation/provider-migration-0.29.md +2 -2
- package/examples/worker/AGENTS.md +37 -9
- package/examples/worker/README.md +127 -61
- package/examples/worker/src/connecta.config.ts +40 -23
- package/examples/worker/wrangler.jsonc +4 -1
- package/package.json +19 -18
- package/templates/node/.env.example +3 -0
- package/templates/node/AGENTS.md +33 -6
- package/templates/node/Dockerfile +2 -2
- package/templates/node/README.md +80 -52
- package/templates/node/docker-compose.yml +3 -2
- package/templates/node/package.json +4 -3
- package/templates/node/src/adopt-token.ts +23 -0
- package/templates/node/src/connecta.config.ts +28 -12
- package/templates/node/src/rotate-token.ts +17 -0
- package/templates/node/src/provision-token.ts +0 -17
package/dist/provider.js
CHANGED
|
@@ -3,17 +3,53 @@
|
|
|
3
3
|
// acquire the MCP client, OAuth, or Effect graph by using it, and a hosted
|
|
4
4
|
// provider does not acquire `api()`. Its runtime imports validate plain
|
|
5
5
|
// options and reviewed classification, with no I/O.
|
|
6
|
-
import { array, assertKnownOptions, keys,
|
|
6
|
+
import { array, assertKnownOptions, ConfigError, fn, keys, object, opaque, required, whole, } from "./config-schema.js";
|
|
7
7
|
import { reviewedClassification } from "./catalog-drift.js";
|
|
8
|
-
const budget =
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const budget = {
|
|
9
|
+
...object({
|
|
10
|
+
kind: required(opaque({
|
|
11
|
+
check(value, path) {
|
|
12
|
+
if (value !== "rolling-window")
|
|
13
|
+
throw new ConfigError(`${path} must be "rolling-window".`);
|
|
14
|
+
},
|
|
15
|
+
})),
|
|
16
|
+
maxCalls: required(whole({ min: 1 })),
|
|
17
|
+
windowMs: required(whole({ min: 1 })),
|
|
18
|
+
}),
|
|
19
|
+
absent: () => undefined,
|
|
20
|
+
};
|
|
21
|
+
const admissionRule = object({
|
|
22
|
+
maxConcurrency: whole({ min: 1 }),
|
|
23
|
+
maxQueueSize: whole({ min: 0 }),
|
|
24
|
+
queueTimeoutMs: whole({ min: 1 }),
|
|
25
|
+
retryAfterMs: whole({ min: 0 }),
|
|
26
|
+
partitionKey: fn(),
|
|
11
27
|
budget,
|
|
12
28
|
});
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
rules: array(admissionRule),
|
|
29
|
+
const admissionPolicy = object({
|
|
30
|
+
maxPartitions: whole({ min: 1 }),
|
|
31
|
+
rules: required(array(admissionRule)),
|
|
16
32
|
});
|
|
33
|
+
export const CALL_ADMISSION = {
|
|
34
|
+
...admissionPolicy,
|
|
35
|
+
absent: () => undefined,
|
|
36
|
+
parse(value, path) {
|
|
37
|
+
const policy = admissionPolicy.parse(assertKnownOptions(value, path, admissionPolicy), path);
|
|
38
|
+
if (policy.rules.length !== 1) {
|
|
39
|
+
throw new ConfigError(`${path}.rules must contain exactly one rule in this release.`);
|
|
40
|
+
}
|
|
41
|
+
const rule = policy.rules[0];
|
|
42
|
+
if (rule.maxConcurrency === undefined && rule.budget === undefined) {
|
|
43
|
+
throw new ConfigError(`${path} rule must declare maxConcurrency or budget.`);
|
|
44
|
+
}
|
|
45
|
+
if (rule.maxConcurrency === undefined &&
|
|
46
|
+
(rule.maxQueueSize !== undefined || rule.queueTimeoutMs !== undefined || rule.retryAfterMs !== undefined)) {
|
|
47
|
+
throw new ConfigError(`${path} queue settings require maxConcurrency.`);
|
|
48
|
+
}
|
|
49
|
+
// object() omits undefined children; its resolved type lists every key.
|
|
50
|
+
return policy;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
17
53
|
/** Options every maintained provider shares. */
|
|
18
54
|
export const PROVIDER_COMMON = {
|
|
19
55
|
...keys("title", "authScope", "purpose", "instructions", "maxResultBytes"),
|
|
@@ -142,6 +178,9 @@ function asProvider(provider, shape, id, options, build) {
|
|
|
142
178
|
let connector;
|
|
143
179
|
try {
|
|
144
180
|
options = assertKnownOptions(options, `${factory}(${JSON.stringify(id)})`, shape);
|
|
181
|
+
const admission = options == null ? undefined : options.callAdmission;
|
|
182
|
+
if (admission !== undefined)
|
|
183
|
+
CALL_ADMISSION.parse(admission, `${factory}(${JSON.stringify(id)}).callAdmission`);
|
|
145
184
|
connector = build(id, options);
|
|
146
185
|
}
|
|
147
186
|
catch (error) {
|
|
@@ -10,9 +10,16 @@
|
|
|
10
10
|
* channel (PRINCIPLES.md INV-3, #678): deployment config supplies
|
|
11
11
|
* `subject`, a function from the admitted `AuthenticatedIdentity` to a
|
|
12
12
|
* Workspace address, and the provider reads the identity core attached beside
|
|
13
|
-
* the context.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* the context. An agent, which has no Workspace account, acts instead as the
|
|
14
|
+
* `owner` of the pool it came through, which core copies from that pool's
|
|
15
|
+
* config beside the identity; the mapping never sees an agent. In a pool
|
|
16
|
+
* without an owner the connector is absent for agents (core's scoped
|
|
17
|
+
* registry view hides it), and this provider still fails `auth_required` if
|
|
18
|
+
* one reaches it anyway (decisions/0006). A fixed
|
|
19
|
+
* `subject` address is every caller's, owners aside. No tool argument,
|
|
20
|
+
* header, or program can name the subject — every product's base URL is
|
|
21
|
+
* confined beneath `users/me`, or its product's equivalent, so the token's
|
|
22
|
+
* subject is the only account a request reaches.
|
|
16
23
|
* A call with no admitted caller, one an open deployment admitted without
|
|
17
24
|
* authentication (the mapping is never asked about it), or one the mapping
|
|
18
25
|
* answers `undefined` for, fails `auth_required` before anything leaves the
|
|
@@ -41,7 +48,8 @@
|
|
|
41
48
|
* that client ID and *exactly* the scopes each product provider lists, comma
|
|
42
49
|
* separated. A grant can take up to 24 hours to apply.
|
|
43
50
|
* 6. Pass the key to the provider from the deployment's secrets, and map the
|
|
44
|
-
* admitted identity to a Workspace address in `subject`.
|
|
51
|
+
* admitted identity to a Workspace address in `subject`. Give each pool
|
|
52
|
+
* agents use an `owner`, the address they act as there.
|
|
45
53
|
*/
|
|
46
54
|
import { type GuardedRequest } from "../../../connectors/guarded-fetch.js";
|
|
47
55
|
import type { AuthenticatedIdentity, ConnectorCallAdmissionPolicy, ConnectorContext } from "../../../types.js";
|
|
@@ -53,13 +61,15 @@ export type { GoogleServiceAccount } from "./service-account.js";
|
|
|
53
61
|
* A function receives the identity an inbound auth provider authenticated for
|
|
54
62
|
* this request and returns that person's Workspace address, or `undefined`
|
|
55
63
|
* for a caller who has none — that call then fails closed. It is never called
|
|
56
|
-
* for an open deployment's anonymous requests,
|
|
57
|
-
* admitted
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
64
|
+
* for an open deployment's anonymous requests, for a call no request
|
|
65
|
+
* admitted, or for an agent: an agent acts as the `owner` of the pool it
|
|
66
|
+
* came through, and never sees the connector in a pool without one. It may be async, for
|
|
67
|
+
* a directory lookup (a Clerk user id to an email, say), and receives the
|
|
68
|
+
* call's cancellation signal for that; it runs once per tool call and should
|
|
69
|
+
* cache what it fetches. A string is one fixed account for every caller who
|
|
70
|
+
* can reach the connector, agents included and pool owners aside, for a
|
|
71
|
+
* shared mailbox or scheduled work with no caller; limit who reaches it with
|
|
72
|
+
* `maintainers` and the `members` of the pools that list it.
|
|
63
73
|
*/
|
|
64
74
|
export type GoogleWorkspaceSubject = string | ((identity: Readonly<AuthenticatedIdentity>, context: GoogleSubjectContext) => string | undefined | Promise<string | undefined>);
|
|
65
75
|
/** What a subject mapping is told about the call it resolves for. */
|
|
@@ -103,18 +113,41 @@ export declare const GOOGLE_WORKSPACE_OPTIONS: import("../../../config-schema.js
|
|
|
103
113
|
readonly privateKey: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
104
114
|
};
|
|
105
115
|
};
|
|
106
|
-
callAdmission: import("../../../config-schema.js").Field<unknown,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
maxPartitions: import("../../../config-schema.js").Field<
|
|
116
|
+
callAdmission: Omit<import("../../../config-schema.js").Field<unknown, ConnectorCallAdmissionPolicy | undefined, true>, "parse"> & {
|
|
117
|
+
parse(value: unknown, path: string): ConnectorCallAdmissionPolicy;
|
|
118
|
+
readonly shape: import("../../../config-schema.js").Shape & {
|
|
119
|
+
maxPartitions: import("../../../config-schema.js").Field<number, number | undefined, true>;
|
|
120
|
+
rules: import("../../../config-schema.js").Field<{
|
|
121
|
+
maxConcurrency?: number | undefined;
|
|
122
|
+
maxQueueSize?: number | undefined;
|
|
123
|
+
queueTimeoutMs?: number | undefined;
|
|
124
|
+
retryAfterMs?: number | undefined;
|
|
125
|
+
partitionKey?: ((input: Readonly<import("../../../types.js").ConnectorCallAdmissionInput>) => string) | undefined;
|
|
126
|
+
budget?: {
|
|
127
|
+
kind: "rolling-window";
|
|
128
|
+
maxCalls: number;
|
|
129
|
+
windowMs: number;
|
|
130
|
+
} | undefined;
|
|
131
|
+
}[], readonly {
|
|
132
|
+
readonly maxConcurrency: number | undefined;
|
|
133
|
+
readonly maxQueueSize: number | undefined;
|
|
134
|
+
readonly queueTimeoutMs: number | undefined;
|
|
135
|
+
readonly retryAfterMs: number | undefined;
|
|
136
|
+
readonly partitionKey: ((input: Readonly<import("../../../types.js").ConnectorCallAdmissionInput>) => string) | undefined;
|
|
137
|
+
readonly budget: {
|
|
138
|
+
readonly kind: "rolling-window";
|
|
139
|
+
readonly maxCalls: number;
|
|
140
|
+
readonly windowMs: number;
|
|
141
|
+
} | undefined;
|
|
142
|
+
}[], false>;
|
|
110
143
|
};
|
|
111
144
|
};
|
|
112
145
|
instructions: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
113
146
|
title: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
114
147
|
maxResultBytes: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
115
|
-
baseUrl: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
116
|
-
subject: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
117
148
|
purpose: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
149
|
+
subject: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
150
|
+
baseUrl: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
118
151
|
};
|
|
119
152
|
};
|
|
120
153
|
/** The checked, construction-time half of {@link GoogleWorkspaceOptions}. */
|
|
@@ -125,6 +158,12 @@ export interface WorkspaceConnection {
|
|
|
125
158
|
}
|
|
126
159
|
/** Validate the shared options at construction; throws on a structural mistake. */
|
|
127
160
|
export declare function workspaceConnection(factory: string, options: GoogleWorkspaceOptions): WorkspaceConnection;
|
|
161
|
+
/**
|
|
162
|
+
* Tag a built Workspace connector with whose account it acts as, so the
|
|
163
|
+
* operator matrix can say "humans act as themselves, agents as the pool
|
|
164
|
+
* owner" for a mapping, or name a fixed subject (#837). Returns it unchanged.
|
|
165
|
+
*/
|
|
166
|
+
export declare function workspaceConnector<C extends object>(connection: WorkspaceConnection, connector: C): C;
|
|
128
167
|
/** How a product reads an HTTP 404 — see H11. */
|
|
129
168
|
type NotFoundMeaning = "absent" | "ambiguous";
|
|
130
169
|
/**
|
|
@@ -10,9 +10,16 @@
|
|
|
10
10
|
* channel (PRINCIPLES.md INV-3, #678): deployment config supplies
|
|
11
11
|
* `subject`, a function from the admitted `AuthenticatedIdentity` to a
|
|
12
12
|
* Workspace address, and the provider reads the identity core attached beside
|
|
13
|
-
* the context.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* the context. An agent, which has no Workspace account, acts instead as the
|
|
14
|
+
* `owner` of the pool it came through, which core copies from that pool's
|
|
15
|
+
* config beside the identity; the mapping never sees an agent. In a pool
|
|
16
|
+
* without an owner the connector is absent for agents (core's scoped
|
|
17
|
+
* registry view hides it), and this provider still fails `auth_required` if
|
|
18
|
+
* one reaches it anyway (decisions/0006). A fixed
|
|
19
|
+
* `subject` address is every caller's, owners aside. No tool argument,
|
|
20
|
+
* header, or program can name the subject — every product's base URL is
|
|
21
|
+
* confined beneath `users/me`, or its product's equivalent, so the token's
|
|
22
|
+
* subject is the only account a request reaches.
|
|
16
23
|
* A call with no admitted caller, one an open deployment admitted without
|
|
17
24
|
* authentication (the mapping is never asked about it), or one the mapping
|
|
18
25
|
* answers `undefined` for, fails `auth_required` before anything leaves the
|
|
@@ -41,13 +48,16 @@
|
|
|
41
48
|
* that client ID and *exactly* the scopes each product provider lists, comma
|
|
42
49
|
* separated. A grant can take up to 24 hours to apply.
|
|
43
50
|
* 6. Pass the key to the provider from the deployment's secrets, and map the
|
|
44
|
-
* admitted identity to a Workspace address in `subject`.
|
|
51
|
+
* admitted identity to a Workspace address in `subject`. Give each pool
|
|
52
|
+
* agents use an `owner`, the address they act as there.
|
|
45
53
|
*/
|
|
46
54
|
import { guardedFetch, retryAfterMs } from "../../../connectors/guarded-fetch.js";
|
|
47
55
|
import { keys, optionsOf } from "../../../config-schema.js";
|
|
48
56
|
import { callerOf } from "../../../connector-caller.js";
|
|
49
57
|
import { CALL_ADMISSION } from "../../../connectors/option-shapes.js";
|
|
50
58
|
import { ConnectorCallError } from "../../../errors.js";
|
|
59
|
+
import { isWorkspaceAddress } from "../../../workspace-address.js";
|
|
60
|
+
import { tagWorkspaceSubject } from "../../../workspace-subject.js";
|
|
51
61
|
import { delegatedToken, forgetDelegatedToken, parseServiceAccount, } from "./service-account.js";
|
|
52
62
|
/** The closed options every Workspace provider accepts; see `assertKnownOptions`. */
|
|
53
63
|
export const GOOGLE_WORKSPACE_OPTIONS = optionsOf()({
|
|
@@ -56,8 +66,6 @@ export const GOOGLE_WORKSPACE_OPTIONS = optionsOf()({
|
|
|
56
66
|
serviceAccount: optionsOf()(keys("clientEmail", "privateKey", "clientId")),
|
|
57
67
|
callAdmission: CALL_ADMISSION,
|
|
58
68
|
});
|
|
59
|
-
/** A plausible mailbox: one `@`, no whitespace or control characters. */
|
|
60
|
-
const ADDRESS = /^[^\s@<>()",;:\\[\]]+@[^\s@<>()",;:\\[\]]+\.[^\s@<>()",;:\\[\]]+$/;
|
|
61
69
|
/** Validate the shared options at construction; throws on a structural mistake. */
|
|
62
70
|
export function workspaceConnection(factory, options) {
|
|
63
71
|
const purpose = options?.purpose?.trim();
|
|
@@ -66,7 +74,7 @@ export function workspaceConnection(factory, options) {
|
|
|
66
74
|
const account = parseServiceAccount(`${factory}()`, options.serviceAccount);
|
|
67
75
|
const subject = options.subject;
|
|
68
76
|
if (typeof subject === "string") {
|
|
69
|
-
if (!
|
|
77
|
+
if (!isWorkspaceAddress(subject.trim())) {
|
|
70
78
|
throw new Error(`${factory}() subject must be a Workspace email address or a function of the caller.`);
|
|
71
79
|
}
|
|
72
80
|
return { purpose, account, subject: subject.trim() };
|
|
@@ -76,6 +84,14 @@ export function workspaceConnection(factory, options) {
|
|
|
76
84
|
}
|
|
77
85
|
return { purpose, account, subject };
|
|
78
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Tag a built Workspace connector with whose account it acts as, so the
|
|
89
|
+
* operator matrix can say "humans act as themselves, agents as the pool
|
|
90
|
+
* owner" for a mapping, or name a fixed subject (#837). Returns it unchanged.
|
|
91
|
+
*/
|
|
92
|
+
export function workspaceConnector(connection, connector) {
|
|
93
|
+
return tagWorkspaceSubject(connector, connection.subject);
|
|
94
|
+
}
|
|
79
95
|
/**
|
|
80
96
|
* The Workspace address this call acts as, resolved from config and the
|
|
81
97
|
* admitted caller only. Throws before any network call when there is none.
|
|
@@ -92,6 +108,16 @@ async function subjectFor(provider, subject, ctx) {
|
|
|
92
108
|
if (!caller.authenticated) {
|
|
93
109
|
throw new ConnectorCallError("auth_required", `${provider} acts as the signed-in Workspace user, and this deployment admitted the call without authentication. Configure inbound auth, or a fixed subject for shared use.`);
|
|
94
110
|
}
|
|
111
|
+
// An agent has no Workspace account of its own. It acts as the owner of the
|
|
112
|
+
// pool it came through, which core copied from that pool's config, and the
|
|
113
|
+
// mapping, which answers for people, is never asked about it (decisions/0006).
|
|
114
|
+
// Core's scoped view already hides the connector from an agent without an
|
|
115
|
+
// owner; this refusal is the fallback for any context built without it.
|
|
116
|
+
if (caller.identity.agent !== undefined) {
|
|
117
|
+
if (caller.pool !== undefined && isWorkspaceAddress(caller.poolOwner))
|
|
118
|
+
return caller.poolOwner;
|
|
119
|
+
throw new ConnectorCallError("auth_required", `${provider} acts as a pool's owner for agents, and the pool this agent reached it through has no owner. An operator sets owner on the pool in the deployment's config; nothing a call sends can choose one.`);
|
|
120
|
+
}
|
|
95
121
|
let mapped;
|
|
96
122
|
try {
|
|
97
123
|
mapped = await subject(caller.identity, ctx.signal ? { signal: ctx.signal } : {});
|
|
@@ -104,7 +130,7 @@ async function subjectFor(provider, subject, ctx) {
|
|
|
104
130
|
if (mapped === undefined) {
|
|
105
131
|
throw new ConnectorCallError("auth_required", `This caller has no Google Workspace account mapped for ${provider}. An operator maps signed-in identities to Workspace addresses in the deployment's subject config; nothing a call sends can choose one.`);
|
|
106
132
|
}
|
|
107
|
-
if (typeof mapped !== "string" || !
|
|
133
|
+
if (typeof mapped !== "string" || !isWorkspaceAddress(mapped.trim())) {
|
|
108
134
|
throw new ConnectorCallError("connector_call_failed", `The deployment's ${provider} subject mapping returned something that is not an email address.`, { retryable: false });
|
|
109
135
|
}
|
|
110
136
|
return mapped.trim();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { guardedFetch, } from "../../../connectors/guarded-fetch.js";
|
|
2
|
-
import { recordRecovery } from "../../../call-recovery.js";
|
|
2
|
+
import { IDEMPOTENCY_KEY_MAX_LENGTH, recordRecovery } from "../../../call-recovery.js";
|
|
3
3
|
import { ConnectorCallError, unavailableCallError } from "../../../errors.js";
|
|
4
4
|
const READ = { readOnlyHint: true };
|
|
5
5
|
const DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true };
|
|
@@ -506,7 +506,7 @@ export function restTools(vendor) {
|
|
|
506
506
|
idempotencyKey: {
|
|
507
507
|
type: "string",
|
|
508
508
|
minLength: 1,
|
|
509
|
-
maxLength:
|
|
509
|
+
maxLength: IDEMPOTENCY_KEY_MAX_LENGTH,
|
|
510
510
|
description: "Reuse to retry this exact write; one is generated and returned when omitted.",
|
|
511
511
|
},
|
|
512
512
|
}
|
|
@@ -99,6 +99,16 @@ interface ResourceRules {
|
|
|
99
99
|
export interface ValueSafetyTable {
|
|
100
100
|
/** Display name in refusals. */
|
|
101
101
|
readonly title: string;
|
|
102
|
+
/** Global signing names plus generic credential names only on these reviewed response paths. */
|
|
103
|
+
readonly embeddedUrls?: {
|
|
104
|
+
readonly operations?: Readonly<Record<string, readonly string[]>>;
|
|
105
|
+
/** Discriminated resources, wherever lists, expansions, or events embed them. */
|
|
106
|
+
readonly resources?: {
|
|
107
|
+
readonly key: string;
|
|
108
|
+
readonly partials?: readonly string[];
|
|
109
|
+
readonly paths: Readonly<Record<string, readonly string[]>>;
|
|
110
|
+
};
|
|
111
|
+
};
|
|
102
112
|
/** The reviewed verdict for every flagged operation, keyed `METHOD /path template`. */
|
|
103
113
|
readonly operations: Readonly<Record<string, ValueSafetyVerdict>>;
|
|
104
114
|
/**
|
|
@@ -127,6 +137,46 @@ export declare function under(prefixes: readonly string[], paths: readonly strin
|
|
|
127
137
|
* (Cloudflare's).
|
|
128
138
|
*/
|
|
129
139
|
export declare function credentialName(name: string): boolean;
|
|
140
|
+
/** Where a vendor stores the files it signs. */
|
|
141
|
+
export interface FileHosts {
|
|
142
|
+
/** Hosts matched whole or as a parent domain: `file.notion.so` covers `img.file.notion.so`. */
|
|
143
|
+
readonly domains: readonly string[];
|
|
144
|
+
/** S3 buckets, virtual-hosted or as the first path segment on an S3 endpoint. Never all of S3. */
|
|
145
|
+
readonly buckets: readonly string[];
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* `text` with every pre-signed link withheld: at one of `hosts` (Notion's
|
|
149
|
+
* file storage, say), its whole query and fragment; anywhere, a signing
|
|
150
|
+
* parameter's value. Everything else comes back byte for byte.
|
|
151
|
+
*/
|
|
152
|
+
export declare function withholdSignedLinks(text: string, hosts: FileHosts): string;
|
|
153
|
+
/**
|
|
154
|
+
* Reviewed REST text policy: only credential-bearing URL queries/fragments
|
|
155
|
+
* change. Ordinary links, including external proxies, retain their bytes.
|
|
156
|
+
* Literal AWS/Google signing names are withheld anywhere, as in #818.
|
|
157
|
+
*/
|
|
158
|
+
export declare function withholdEmbeddedUrls(text: string): string;
|
|
159
|
+
/** Every string inside a JSON value through `withholdSignedLinks`; unchanged values keep their identity. */
|
|
160
|
+
export declare function withholdSignedLinksIn(value: unknown, hosts: FileHosts, depth?: number): unknown;
|
|
161
|
+
/** Apply the reviewed text policy to a selected value or vendor error body. */
|
|
162
|
+
export declare function withholdEmbeddedUrlsIn(value: unknown): unknown;
|
|
163
|
+
/**
|
|
164
|
+
* Whether a request value carries a link whose query or
|
|
165
|
+
* fragment holds the exact `[redacted]` placeholder, plain or encoded: a
|
|
166
|
+
* withheld read copied into a write, which would store the placeholder.
|
|
167
|
+
* Only the link's own destination is read: a Markdown link ends at its
|
|
168
|
+
* closing `)`, so `[f](…?a=1)[redacted]` is ordinary text after it.
|
|
169
|
+
* The word alone (`unredacted`) or the placeholder away from a file link is
|
|
170
|
+
* a person's text. When `hosts` is supplied, require its actual authority
|
|
171
|
+
* to match that policy, preserving Notion's external-proxy boundary.
|
|
172
|
+
*/
|
|
173
|
+
export declare function carriesWithheldLink(value: unknown, hosts?: FileHosts): boolean;
|
|
174
|
+
/** Refuse a withheld destination copied into customer data, before any dispatch. */
|
|
175
|
+
export declare function refuseWithheldLinks(value: unknown, title: string): void;
|
|
176
|
+
/** Upload admission inspects actual bytes, including base64-decoded files and multipart blobs. */
|
|
177
|
+
export declare function refuseWithheldUpload(body: BodyInit, title: string): Promise<void>;
|
|
178
|
+
/** Only for documented JSON fields the vendor decodes as base64, never arbitrary strings. */
|
|
179
|
+
export declare function refuseWithheldBase64(value: unknown, title: string, maxDecodedBytes?: number): Promise<void>;
|
|
130
180
|
/** Whether every segment of a reviewed path is in the path language; the harness checks each table. */
|
|
131
181
|
export declare function validPath(raw: string): boolean;
|
|
132
182
|
/** What a vendor's tools ask of its reviewed table. */
|