@zackbart/connecta 0.32.0 → 0.33.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 +63 -0
- package/README.md +15 -7
- 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/agent-identity.js +47 -0
- package/dist/auth/cloudflare-access.js +13 -5
- package/dist/call-admission-context.js +15 -0
- package/dist/call-admission.js +30 -43
- package/dist/call-recovery.js +6 -2
- package/dist/config-value-sources.js +11 -9
- package/dist/config.d.ts +37 -75
- package/dist/config.js +110 -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 +3 -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 +29 -7
- package/dist/describe-config.js +7 -17
- package/dist/index.d.ts +4 -3
- package/dist/index.js +29 -10
- package/dist/invocation.js +5 -3
- package/dist/membership.d.ts +51 -0
- package/dist/membership.js +121 -0
- package/dist/module-contracts.d.ts +23 -6
- 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/contract.d.ts +149 -28
- package/dist/operator-ui/endpoints.js +192 -0
- package/dist/operator-ui/fix-prompts.js +4 -4
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/member-page.js +112 -0
- package/dist/operator-ui/model.d.ts +14 -6
- package/dist/operator-ui/view.d.ts +84 -19
- package/dist/operator-ui/view.js +82 -21
- package/dist/page-styles.js +1 -1
- package/dist/provider.d.ts +61 -8
- package/dist/provider.js +46 -7
- package/dist/providers/_shared/google/workspace.d.ts +53 -16
- package/dist/providers/_shared/google/workspace.js +30 -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 +1 -1
- 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 +3 -3
- package/dist/providers/docs/skill.generated.js +2 -2
- package/dist/providers/drive/index.js +3 -3
- package/dist/providers/drive/skill.generated.js +2 -2
- package/dist/providers/forms/index.js +3 -3
- package/dist/providers/forms/skill.generated.js +2 -2
- package/dist/providers/gmail/index.js +10 -8
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/mixpanel/index.js +1 -1
- 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 +1 -1
- package/dist/providers/planning-center/index.js +1 -1
- package/dist/providers/sheets/index.d.ts +3 -0
- package/dist/providers/sheets/index.js +6 -3
- package/dist/providers/sheets/skill.generated.js +2 -2
- package/dist/providers/slides/index.js +3 -3
- 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/vercel/rest.js +20 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/value-safety.js +22 -0
- package/dist/registry.d.ts +2 -3
- package/dist/registry.js +8 -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 +145 -95
- package/dist/routes/oauth-management.js +11 -5
- package/dist/routes/oauth.js +3 -9
- 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/storage/keys.js +9 -1
- package/dist/types.d.ts +11 -0
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +16 -14
- 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 +46 -22
- package/documentation/architecture.md +355 -50
- package/documentation/auth.md +775 -243
- package/documentation/code-mode.md +14 -11
- package/documentation/deploying.md +121 -24
- package/documentation/integrating.md +92 -21
- package/documentation/meta-tools.md +14 -9
- package/documentation/migration-0.33.md +344 -0
- package/documentation/operating.md +3 -1
- package/documentation/operator-tests.md +115 -28
- package/documentation/operator-ui.md +326 -61
- package/documentation/provider-auth-migration.md +37 -23
- package/documentation/provider-migration-0.29.md +2 -2
- package/examples/worker/AGENTS.md +30 -8
- package/examples/worker/README.md +92 -53
- package/examples/worker/src/connecta.config.ts +36 -20
- package/examples/worker/wrangler.jsonc +3 -0
- package/package.json +3 -2
- package/templates/node/.env.example +3 -0
- package/templates/node/AGENTS.md +27 -6
- package/templates/node/Dockerfile +2 -2
- package/templates/node/README.md +67 -42
- 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/describe-config.js
CHANGED
|
@@ -183,13 +183,6 @@ function describeConnector(connector, deploymentCap, registry) {
|
|
|
183
183
|
...(connector.staticTools ? { tools: describedTools(registry.describeStaticTools(connector.id) ?? []) } : {}),
|
|
184
184
|
};
|
|
185
185
|
}
|
|
186
|
-
const IDENTITY_KEYS = [
|
|
187
|
-
"connectorAccess",
|
|
188
|
-
"activityAccess",
|
|
189
|
-
"credentialAdministration",
|
|
190
|
-
"accessTokenManagement",
|
|
191
|
-
"personalConnection",
|
|
192
|
-
];
|
|
193
186
|
const STORE_KINDS = new Set(["memory", "d1", "sqlite"]);
|
|
194
187
|
const storeKind = (described) => typeof described?.kind === "string" && STORE_KINDS.has(described.kind)
|
|
195
188
|
? described.kind
|
|
@@ -198,7 +191,6 @@ const storeKind = (described) => typeof described?.kind === "string" && STORE_KI
|
|
|
198
191
|
export function describeConfig(input) {
|
|
199
192
|
const { raw, config } = input;
|
|
200
193
|
const brand = resolveBranding(config.ui?.branding);
|
|
201
|
-
const accessTokens = config.accessTokens?.describe?.();
|
|
202
194
|
const storage = config.storage;
|
|
203
195
|
const activityStore = config.activity?.store.describe?.();
|
|
204
196
|
const D = CONFIG_DEFAULTS;
|
|
@@ -241,20 +233,23 @@ export function describeConfig(input) {
|
|
|
241
233
|
requests: limitGroup(config.admission.requests, ownValue(ownValue(raw, "admission"), "requests"), Object.keys(D.admission.requests)),
|
|
242
234
|
},
|
|
243
235
|
readOnly: config.readOnly,
|
|
244
|
-
|
|
236
|
+
maintainers: input.maintainers.map(({ namespace, id }) => ({ namespace: String(namespace), id: String(id) })),
|
|
245
237
|
classification: structuredClone(config.classification ?? {}),
|
|
246
238
|
auth: config.auth.map((provider) => ({
|
|
247
239
|
kind: String(provider.kind),
|
|
248
240
|
interactive: provider.interactiveOperator === true,
|
|
249
241
|
...(str(provider.uiAuth?.kind) !== undefined ? { ui: provider.uiAuth.kind } : {}),
|
|
250
242
|
})),
|
|
251
|
-
|
|
243
|
+
agents: [...config.agents],
|
|
252
244
|
pools: Object.entries(config.pools ?? {}).map(([name, pool]) => ({
|
|
253
245
|
readOnly: pool.readOnly ?? config.readOnly,
|
|
254
246
|
name,
|
|
255
247
|
path: `/mcp/${name}`,
|
|
256
248
|
tools: pool.tools.map(String),
|
|
257
|
-
|
|
249
|
+
members: (input.pools.get(name)?.members ?? []).map((member) => member.kind === "agent"
|
|
250
|
+
? { kind: "agent", name: member.name }
|
|
251
|
+
: { kind: "human", namespace: member.namespace, id: member.id }),
|
|
252
|
+
owner: input.pools.get(name)?.owner ?? null,
|
|
258
253
|
})),
|
|
259
254
|
modules: {
|
|
260
255
|
ui: { enabled: config.ui !== undefined },
|
|
@@ -277,12 +272,7 @@ export function describeConfig(input) {
|
|
|
277
272
|
},
|
|
278
273
|
}
|
|
279
274
|
: { enabled: false },
|
|
280
|
-
accessTokens: config.accessTokens
|
|
281
|
-
? {
|
|
282
|
-
enabled: true,
|
|
283
|
-
...(num(accessTokens?.maxActive) !== undefined ? { maxActive: accessTokens.maxActive } : {}),
|
|
284
|
-
}
|
|
285
|
-
: { enabled: false },
|
|
275
|
+
accessTokens: { enabled: config.accessTokens !== undefined },
|
|
286
276
|
},
|
|
287
277
|
storage: {
|
|
288
278
|
configured: raw.storage !== undefined,
|
package/dist/index.d.ts
CHANGED
|
@@ -5,8 +5,9 @@ export { customExecutor, type CustomExecutorOptions } from "./executor-contract.
|
|
|
5
5
|
export type { AccessTokensModule, ActivityModule, OperatorSurface } from "./module-contracts.js";
|
|
6
6
|
export type { CredentialVault, CredentialMetadata } from "./credential-contract.js";
|
|
7
7
|
export { defineConfig } from "./config.js";
|
|
8
|
-
export type { AdmissionPoolConfig, ConnectaAdmissionConfig, ConnectaCallsConfig, ConnectaConfig, ConnectaDiscoveryConfig, ConnectaExecuteConfig,
|
|
9
|
-
export type {
|
|
8
|
+
export type { AdmissionPoolConfig, ConnectaAdmissionConfig, ConnectaCallsConfig, ConnectaConfig, ConnectaDiscoveryConfig, ConnectaExecuteConfig, ConnectaPoolConfig, ConnectaResultsConfig, RequestAdmissionConfig, } from "./config.js";
|
|
9
|
+
export type { MemberRef } from "./membership.js";
|
|
10
|
+
export type { ConfigValueSource, ConnectaConfigDescription, DescribedConnector, DescribedMember, DescribedLimit, } from "./describe-config.js";
|
|
10
11
|
export interface Connecta {
|
|
11
12
|
/** Web-standard fetch handler. Usable as `export default { fetch: connecta.fetch }`. */
|
|
12
13
|
fetch: (request: Request, env?: unknown, ctx?: unknown) => Promise<Response>;
|
|
@@ -14,7 +15,7 @@ export interface Connecta {
|
|
|
14
15
|
/**
|
|
15
16
|
* A secret-free snapshot of the configuration this deployment runs with,
|
|
16
17
|
* built once at construction: limits and where each came from, auth
|
|
17
|
-
* providers,
|
|
18
|
+
* providers, maintainers, agents, pools, modules, and every connector's source,
|
|
18
19
|
* endpoint, auth mode, and static tools. Header values, keys, client
|
|
19
20
|
* secrets, credentials, and functions never appear. The same object is
|
|
20
21
|
* returned on every call.
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,9 @@ import { describeConfigSources } from "./config-value-sources.js";
|
|
|
2
2
|
import { credentialTestRule, describeCredentialTestMismatch } from "./credential-rules.js";
|
|
3
3
|
import { Registry } from "./registry.js";
|
|
4
4
|
import { parseConnectorAccess, POOL_NAME_RE } from "./connector-access.js";
|
|
5
|
+
import { assertsAgents } from "./agent-identity.js";
|
|
6
|
+
import { ConfigError } from "./config-schema.js";
|
|
7
|
+
import { memberContext, resolveMaintainers, resolveMembers, } from "./membership.js";
|
|
5
8
|
import { createFetchHandler } from "./server.js";
|
|
6
9
|
import { droppedBrandingUrls, droppedThemeTokens, droppedUiAuthUrls } from "./branding.js";
|
|
7
10
|
import { readConfig } from "./config.js";
|
|
@@ -10,19 +13,21 @@ export { customExecutor } from "./executor-contract.js";
|
|
|
10
13
|
import { AdmissionController, executorName, isAdmittingExecutor, withExecutorAdmission } from "./executor-admission.js";
|
|
11
14
|
export { defineConfig } from "./config.js";
|
|
12
15
|
/**
|
|
13
|
-
* Validate declared pools against the connector set
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Validate declared pools against the connector set and the members against
|
|
17
|
+
* auth and agents. Everything checkable at construction throws here: a
|
|
18
|
+
* malformed name, an unparseable grant, an unknown connector id, a tool
|
|
19
|
+
* address on an `api()` connector whose static catalog lacks it, and an empty,
|
|
20
|
+
* ambiguous, unknown, or repeated member. Remote catalogs load lazily, so
|
|
21
|
+
* their addresses are checked at catalog load instead and stay unreachable
|
|
22
|
+
* until they match.
|
|
18
23
|
*/
|
|
19
|
-
function resolvePools(pools, readOnly, registry) {
|
|
24
|
+
function resolvePools(pools, readOnly, registry, members) {
|
|
20
25
|
const resolved = new Map();
|
|
21
26
|
if (!pools)
|
|
22
27
|
return resolved;
|
|
23
28
|
// The schema has already refused a pool that is not an object, a missing or
|
|
24
|
-
// non-array `tools
|
|
25
|
-
//
|
|
29
|
+
// non-array `tools` or `members`, and a misspelled key; the retired `grant`
|
|
30
|
+
// is refused there with its replacement named.
|
|
26
31
|
for (const [name, pool] of Object.entries(pools)) {
|
|
27
32
|
if (!POOL_NAME_RE.test(name)) {
|
|
28
33
|
throw new Error(`ConnectaConfig.pools: pool name "${name}" must match [a-z0-9_-]+`);
|
|
@@ -52,8 +57,17 @@ function resolvePools(pools, readOnly, registry) {
|
|
|
52
57
|
}
|
|
53
58
|
}
|
|
54
59
|
}
|
|
60
|
+
const path = `ConnectaConfig.pools.${name}.members`;
|
|
61
|
+
if (pool.members.length === 0) {
|
|
62
|
+
throw new ConfigError(`${path} is empty; a pool serves only its members, so list at least one.`);
|
|
63
|
+
}
|
|
55
64
|
// A pool inherits the deployment value unless it sets its own, either way.
|
|
56
|
-
resolved.set(name,
|
|
65
|
+
resolved.set(name, Object.freeze({
|
|
66
|
+
access,
|
|
67
|
+
readOnly: pool.readOnly ?? readOnly,
|
|
68
|
+
members: Object.freeze(resolveMembers(pool.members, path, members)),
|
|
69
|
+
...(pool.owner !== undefined ? { owner: pool.owner.trim() } : {}),
|
|
70
|
+
}));
|
|
57
71
|
}
|
|
58
72
|
return resolved;
|
|
59
73
|
}
|
|
@@ -191,7 +205,9 @@ export function createConnecta(config) {
|
|
|
191
205
|
results: resolved.results,
|
|
192
206
|
classification: resolved.classification,
|
|
193
207
|
});
|
|
194
|
-
const
|
|
208
|
+
const members = memberContext(resolved.auth, resolved.agents);
|
|
209
|
+
const maintainers = resolveMaintainers(input.maintainers, resolved.auth.some((provider) => !assertsAgents(provider)), members);
|
|
210
|
+
const pools = resolvePools(resolved.pools, resolved.readOnly, registry, members);
|
|
195
211
|
warnInsecureConfig(resolved);
|
|
196
212
|
const requestAdmission = new AdmissionController(resolved.admission.requests);
|
|
197
213
|
let codeAdmission;
|
|
@@ -214,6 +230,8 @@ export function createConnecta(config) {
|
|
|
214
230
|
registry,
|
|
215
231
|
raw: input,
|
|
216
232
|
config: resolved,
|
|
233
|
+
maintainers,
|
|
234
|
+
pools,
|
|
217
235
|
executorName: configuredExecutorName,
|
|
218
236
|
executorAdmits,
|
|
219
237
|
});
|
|
@@ -222,6 +240,7 @@ export function createConnecta(config) {
|
|
|
222
240
|
configDescription: description,
|
|
223
241
|
configValueSources: describeConfigSources(description, input),
|
|
224
242
|
registry,
|
|
243
|
+
maintainers,
|
|
225
244
|
pools,
|
|
226
245
|
executor,
|
|
227
246
|
executorName: configuredExecutorName,
|
package/dist/invocation.js
CHANGED
|
@@ -3,6 +3,7 @@ import { hasControlCharacters } from "./tool-name.js";
|
|
|
3
3
|
import { writeRefusal } from "./tool-safety.js";
|
|
4
4
|
import { Cause, Effect, Exit } from "effect";
|
|
5
5
|
import { isCallAdmissionError } from "./call-admission.js";
|
|
6
|
+
import { attachCallAdmission } from "./call-admission-context.js";
|
|
6
7
|
import { boundedEchoText, classificationCode, classifyCallError, ConnectorCallError, echoedCallArgs, framingError, } from "./errors.js";
|
|
7
8
|
import { downstreamValue } from "./mcp-result.js";
|
|
8
9
|
import { carryFailureFacts, classifiedFailure, failureRecord, logFailure, recordedToolName, } from "./operator-record.js";
|
|
@@ -494,8 +495,9 @@ export class InvocationService {
|
|
|
494
495
|
if (callSignal?.aborted)
|
|
495
496
|
return yield* Effect.fail(callSignal.reason);
|
|
496
497
|
attempts = 1;
|
|
497
|
-
const call = async () => {
|
|
498
|
+
const call = async (permit) => {
|
|
498
499
|
const connectorContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({ signal: callSignal, timeoutMs: context.timeoutMs, defer: this.catalog.defer }));
|
|
500
|
+
attachCallAdmission(connectorContext, permit);
|
|
499
501
|
trackCredentialReads(connectorContext);
|
|
500
502
|
sentSecrets.include(sentSecretsFor(connectorContext));
|
|
501
503
|
dispatchContext = connectorContext;
|
|
@@ -537,14 +539,14 @@ export class InvocationService {
|
|
|
537
539
|
// cannot hold its permit past the deadline.
|
|
538
540
|
const attempt = yield* Effect.exit(Effect.scoped(Effect.gen({ self: this }, function* () {
|
|
539
541
|
stage = "admission";
|
|
540
|
-
yield* timed((elapsed) => {
|
|
542
|
+
const permit = yield* timed((elapsed) => {
|
|
541
543
|
admissionMs += elapsed;
|
|
542
544
|
}, admitted(this.registry, target, args, admissionSignal));
|
|
543
545
|
stage = "downstream";
|
|
544
546
|
const reply = yield* timed((elapsed) => {
|
|
545
547
|
connectorMs += elapsed;
|
|
546
548
|
}, Effect.tryPromise({
|
|
547
|
-
try: () => Promise.resolve(call()),
|
|
549
|
+
try: () => Promise.resolve(call(permit)),
|
|
548
550
|
catch: (error) => error,
|
|
549
551
|
}));
|
|
550
552
|
if (target.connector.kind === "mcp" && isDownstreamInputResult(reply)) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ConnectorAccess } from "./connector-access.js";
|
|
2
|
+
import type { AuthenticatedIdentity, IdentityReference, InboundAuth } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* A person or an agent, as deployment code lists it: a bare principal id in
|
|
5
|
+
* the namespace of the deployment's one interactive auth provider,
|
|
6
|
+
* `{ namespace, id }` when that is ambiguous or the id contains `:`, or
|
|
7
|
+
* `"agent:<name>"` for an agent declared in `agents`.
|
|
8
|
+
*/
|
|
9
|
+
export type MemberRef = string | Readonly<IdentityReference>;
|
|
10
|
+
/** A resolved member reference. Humans match by principal, agents by name. */
|
|
11
|
+
export type Member = {
|
|
12
|
+
kind: "human";
|
|
13
|
+
namespace: string;
|
|
14
|
+
id: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: "agent";
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
19
|
+
/** A declared pool after construction-time validation. */
|
|
20
|
+
export interface ResolvedPool {
|
|
21
|
+
/** Resolved: the pool's own `readOnly`, else the deployment's. */
|
|
22
|
+
readOnly: boolean;
|
|
23
|
+
/** Exactly the pool's `tools`; nothing intersects or widens it. */
|
|
24
|
+
access: ConnectorAccess;
|
|
25
|
+
members: readonly Member[];
|
|
26
|
+
/** Trimmed `owner`: the Workspace address agents in this pool act as. */
|
|
27
|
+
owner?: string;
|
|
28
|
+
}
|
|
29
|
+
/** What resolving a reference needs to know about the deployment. */
|
|
30
|
+
export interface MemberContext {
|
|
31
|
+
/** `activityActorNamespace` of each interactive provider, `undefined` where it has none. */
|
|
32
|
+
interactiveNamespaces: ReadonlyArray<string | undefined>;
|
|
33
|
+
agents: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
/** The namespaces bare ids may resolve in, read from the resolved providers. */
|
|
36
|
+
export declare function memberContext(auth: readonly InboundAuth[], agents: readonly string[]): MemberContext;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve one list of references, throwing on any that is malformed,
|
|
39
|
+
* ambiguous, unknown, or repeated (INV-11).
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveMembers(value: unknown, path: string, context: MemberContext): Member[];
|
|
42
|
+
/**
|
|
43
|
+
* `maintainers`: people only. Required whenever an inbound provider can admit
|
|
44
|
+
* a person, which is any provider but the built-in agent-token adapter. With
|
|
45
|
+
* agents alone nobody can be a maintainer, so plain `/mcp` admits nobody.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveMaintainers(value: unknown, admitsPeople: boolean, context: MemberContext): readonly IdentityReference[];
|
|
48
|
+
/** Whether an admitted identity is this member. An agent never matches a person. */
|
|
49
|
+
export declare function isMember(member: Member, identity: Readonly<AuthenticatedIdentity>): boolean;
|
|
50
|
+
/** Whether an admitted identity is a listed maintainer. */
|
|
51
|
+
export declare function isMaintainer(maintainers: readonly IdentityReference[], identity: Readonly<AuthenticatedIdentity>): boolean;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Who may open which endpoint (#837, decisions/0006): `maintainers` open plain
|
|
2
|
+
// `/mcp`, and each pool's static `members` open `/mcp/<pool>`. References are
|
|
3
|
+
// resolved once at construction; a request is matched against them and never
|
|
4
|
+
// names its own scope (INV-4). Pure, so core reads it without any auth module.
|
|
5
|
+
import { isAgentName } from "./agent-identity.js";
|
|
6
|
+
import { ConfigError } from "./config-schema.js";
|
|
7
|
+
import { validIdentityReference } from "./identity.js";
|
|
8
|
+
const AGENT_PREFIX = "agent:";
|
|
9
|
+
/** The namespaces bare ids may resolve in, read from the resolved providers. */
|
|
10
|
+
export function memberContext(auth, agents) {
|
|
11
|
+
return {
|
|
12
|
+
interactiveNamespaces: auth
|
|
13
|
+
.filter((provider) => provider.interactiveOperator === true)
|
|
14
|
+
.map((provider) => typeof provider.activityActorNamespace === "string" &&
|
|
15
|
+
validIdentityReference({ namespace: provider.activityActorNamespace, id: "x" })
|
|
16
|
+
? provider.activityActorNamespace
|
|
17
|
+
: undefined),
|
|
18
|
+
agents,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function resolveRef(value, path, context) {
|
|
22
|
+
if (typeof value === "string") {
|
|
23
|
+
if (value.startsWith(AGENT_PREFIX)) {
|
|
24
|
+
const name = value.slice(AGENT_PREFIX.length);
|
|
25
|
+
if (!isAgentName(name) || !context.agents.includes(name)) {
|
|
26
|
+
throw new ConfigError(`${path} names agent "${name}", which ConnectaConfig.agents does not declare.`);
|
|
27
|
+
}
|
|
28
|
+
return { kind: "agent", name };
|
|
29
|
+
}
|
|
30
|
+
if (value.includes(":")) {
|
|
31
|
+
throw new ConfigError(`${path} contains ":". Write a principal whose id contains ":" as { namespace, id }; ` +
|
|
32
|
+
'agents are "agent:<name>".');
|
|
33
|
+
}
|
|
34
|
+
if (!validIdentityReference({ namespace: "x", id: value })) {
|
|
35
|
+
throw new ConfigError(`${path} must be a principal id of 1 to 256 printable, non-space ASCII characters.`);
|
|
36
|
+
}
|
|
37
|
+
const namespaces = context.interactiveNamespaces;
|
|
38
|
+
if (namespaces.length !== 1) {
|
|
39
|
+
throw new ConfigError(`${path} is a bare principal id, which resolves only when exactly one interactive auth provider is ` +
|
|
40
|
+
`configured (found ${namespaces.length}). Write it as { namespace, id }.`);
|
|
41
|
+
}
|
|
42
|
+
const namespace = namespaces[0];
|
|
43
|
+
if (namespace === undefined) {
|
|
44
|
+
throw new ConfigError(`${path} is a bare principal id, but the interactive auth provider declares no activityActorNamespace. ` +
|
|
45
|
+
"Write it as { namespace, id }.");
|
|
46
|
+
}
|
|
47
|
+
return { kind: "human", namespace, id: value };
|
|
48
|
+
}
|
|
49
|
+
if (typeof value !== "object" ||
|
|
50
|
+
value === null ||
|
|
51
|
+
Array.isArray(value) ||
|
|
52
|
+
Object.keys(value).length !== 2 ||
|
|
53
|
+
!Object.hasOwn(value, "namespace") ||
|
|
54
|
+
!Object.hasOwn(value, "id")) {
|
|
55
|
+
throw new ConfigError(`${path} must be a principal id string, { namespace, id }, or "agent:<name>".`);
|
|
56
|
+
}
|
|
57
|
+
const { namespace, id } = value;
|
|
58
|
+
if (typeof namespace !== "string" || typeof id !== "string" || !validIdentityReference({ namespace, id })) {
|
|
59
|
+
throw new ConfigError(`${path} must have a namespace and id of 1 to 256 printable, non-space ASCII characters each.`);
|
|
60
|
+
}
|
|
61
|
+
return { kind: "human", namespace, id };
|
|
62
|
+
}
|
|
63
|
+
const memberKey = (member) => member.kind === "agent" ? `agent\0${member.name}` : `human\0${member.namespace}\0${member.id}`;
|
|
64
|
+
/**
|
|
65
|
+
* Resolve one list of references, throwing on any that is malformed,
|
|
66
|
+
* ambiguous, unknown, or repeated (INV-11).
|
|
67
|
+
*/
|
|
68
|
+
export function resolveMembers(value, path, context) {
|
|
69
|
+
if (!Array.isArray(value)) {
|
|
70
|
+
throw new ConfigError(`${path} must be an array of principal ids, { namespace, id }, or "agent:<name>".`);
|
|
71
|
+
}
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
return value.map((entry, index) => {
|
|
74
|
+
const member = resolveRef(entry, `${path}[${index}]`, context);
|
|
75
|
+
const key = memberKey(member);
|
|
76
|
+
if (seen.has(key))
|
|
77
|
+
throw new ConfigError(`${path}[${index}] repeats an earlier member.`);
|
|
78
|
+
seen.add(key);
|
|
79
|
+
return Object.freeze(member);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* `maintainers`: people only. Required whenever an inbound provider can admit
|
|
84
|
+
* a person, which is any provider but the built-in agent-token adapter. With
|
|
85
|
+
* agents alone nobody can be a maintainer, so plain `/mcp` admits nobody.
|
|
86
|
+
*/
|
|
87
|
+
export function resolveMaintainers(value, admitsPeople, context) {
|
|
88
|
+
const path = "ConnectaConfig.maintainers";
|
|
89
|
+
if (value === undefined || (Array.isArray(value) && value.length === 0)) {
|
|
90
|
+
if (admitsPeople) {
|
|
91
|
+
throw new ConfigError(`${path} is required with inbound auth: list the people who may use plain /mcp, administer shared ` +
|
|
92
|
+
"credentials, manage agent tokens, and read activity.");
|
|
93
|
+
}
|
|
94
|
+
return Object.freeze([]);
|
|
95
|
+
}
|
|
96
|
+
if (!admitsPeople) {
|
|
97
|
+
throw new ConfigError(`${path} needs an inbound auth provider that admits people, such as clerkAuth or cloudflareAccessAuth. ` +
|
|
98
|
+
"Without one no maintainer can be admitted; agents are never maintainers.");
|
|
99
|
+
}
|
|
100
|
+
const members = resolveMembers(value, path, context);
|
|
101
|
+
return Object.freeze(members.map((member, index) => {
|
|
102
|
+
if (member.kind === "agent") {
|
|
103
|
+
throw new ConfigError(`${path}[${index}] is an agent. Agents are never maintainers; add them to a pool's members.`);
|
|
104
|
+
}
|
|
105
|
+
return Object.freeze({ namespace: member.namespace, id: member.id });
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
/** Whether an admitted identity is this member. An agent never matches a person. */
|
|
109
|
+
export function isMember(member, identity) {
|
|
110
|
+
if (member.kind === "agent")
|
|
111
|
+
return identity.agent !== undefined && identity.agent === member.name;
|
|
112
|
+
const principal = identity.principal;
|
|
113
|
+
return (identity.agent === undefined &&
|
|
114
|
+
principal !== undefined &&
|
|
115
|
+
principal.namespace === member.namespace &&
|
|
116
|
+
principal.id === member.id);
|
|
117
|
+
}
|
|
118
|
+
/** Whether an admitted identity is a listed maintainer. */
|
|
119
|
+
export function isMaintainer(maintainers, identity) {
|
|
120
|
+
return maintainers.some((maintainer) => isMember({ kind: "human", ...maintainer }, identity));
|
|
121
|
+
}
|
|
@@ -17,13 +17,30 @@ export interface ActivityModule {
|
|
|
17
17
|
readonly recordTool: typeof recordToolActivity;
|
|
18
18
|
readonly recordChange: typeof recordCatalogChangeActivity;
|
|
19
19
|
}
|
|
20
|
-
/** Optional
|
|
20
|
+
/** Optional agent-token authentication and operator lifecycle. */
|
|
21
21
|
export interface AccessTokensModule {
|
|
22
22
|
readonly auth: import("./types.js").InboundAuth;
|
|
23
23
|
handle(context: RouteContext): Promise<Response | null>;
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Every declared agent's token slot, in declaration order: the list GET
|
|
26
|
+
* /ui/access-tokens serves, which the maintainer's operator contract
|
|
27
|
+
* repeats field by field. Never secret material.
|
|
28
|
+
*/
|
|
29
|
+
slots(): Promise<Array<{
|
|
30
|
+
name: string;
|
|
31
|
+
state: "none";
|
|
32
|
+
revision: string;
|
|
33
|
+
} | {
|
|
34
|
+
name: string;
|
|
35
|
+
state: "issued";
|
|
36
|
+
issuedAt: string;
|
|
37
|
+
revision: string;
|
|
38
|
+
} | {
|
|
39
|
+
name: string;
|
|
40
|
+
state: "revoked";
|
|
41
|
+
revokedAt: string;
|
|
42
|
+
revision: string;
|
|
43
|
+
}>>;
|
|
44
|
+
/** The same module bound to the deployment's declared `agents`; createConnecta calls it once. */
|
|
45
|
+
forAgents(agents: readonly string[]): AccessTokensModule;
|
|
29
46
|
}
|
|
@@ -8,7 +8,7 @@ type FailureStep = "MCP handshake" | "tools/list" | "tools/call" | "resources/re
|
|
|
8
8
|
*/
|
|
9
9
|
export declare const OAUTH_ERROR_CODES: ReadonlySet<string>;
|
|
10
10
|
/** Inbound denial facts, never identity text or provider diagnostics. */
|
|
11
|
-
export type AuthDenialReason = "authentication_failed" | "authorization_header_invalid" | "token_type_mismatch" | "oauth_verification_failed" | "oauth_client_not_allowed" | "oauth_binding_mismatch" | "session_origin_mismatch" | "user_missing" | "email_lookup_failed" | "verified_email_invalid" | "email_domain_denied" | "gate_denied" | "gate_failed" | "
|
|
11
|
+
export type AuthDenialReason = "authentication_failed" | "authorization_header_invalid" | "token_type_mismatch" | "oauth_verification_failed" | "oauth_client_not_allowed" | "oauth_binding_mismatch" | "session_origin_mismatch" | "user_missing" | "email_lookup_failed" | "verified_email_invalid" | "email_domain_denied" | "gate_denied" | "gate_failed" | "not_maintainer" | "pool_not_declared" | "not_pool_member";
|
|
12
12
|
/**
|
|
13
13
|
* Name `ctor`'s instances `label` in records and fixed text. Call once, at
|
|
14
14
|
* module scope, beside the class. An error whose prototype chain reaches no
|
|
@@ -104,7 +104,7 @@ export declare function failureRecord(subject: FailureSubject, failure?: unknown
|
|
|
104
104
|
* The events an operator log reports failures as. Closed: a new one is a
|
|
105
105
|
* reviewed line here, never a string assembled at the call site.
|
|
106
106
|
*/
|
|
107
|
-
export type FailureEvent = "negotiation cache read failed" | "negotiation cache write failed" | "
|
|
107
|
+
export type FailureEvent = "negotiation cache read failed" | "negotiation cache write failed" | "pool tool grant is unreachable" | "call failed" | "input schema unusable; arguments are not validated" | "catalog read failed" | "catalog persistence failed" | "catalog invalidation failed" | "catalog refresh observation failed" | "catalog refresh failed; serving stale catalog" | "deferred catalog refresh failed" | "deferred catalog refresh could not attach to the runtime" | "catalog drift check failed" | "schema digest check failed; serving digested reviews as writes" | "credential shape read failed" | "credential test failed" | "credential test threw" | "session termination refused or failed; the downstream session may remain until its provider timeout" | "activity record failed" | "catalog drift record failed" | "activity read failed" | "OAuth start failed" | "OAuth disconnect failed" | "OAuth callback verifyState threw; no authorization code was exchanged" | "OAuth callback handoff could not be consumed; no authorization code was exchanged" | "MCP default endpoint request denied" | "MCP pool request denied" | "MCP agent endpoint request denied" | "OAuth error callback consent could not be consumed; no authorization code was exchanged" | "MCP handler error" | "operator status" | "operator agent slots read failed" | "result paging unavailable" | "request failed" | "Clerk request denied" | "Clerk authentication failed";
|
|
108
108
|
/**
|
|
109
109
|
* Log one failure as `[connecta] <event>` and its record. A record
|
|
110
110
|
* `failureRecord` did not build is replaced, whatever its shape, by a fixed
|
package/dist/operator-record.js
CHANGED
|
@@ -112,11 +112,9 @@ const AUTH_DENIAL_REASONS = new Set([
|
|
|
112
112
|
"email_domain_denied",
|
|
113
113
|
"gate_denied",
|
|
114
114
|
"gate_failed",
|
|
115
|
-
"
|
|
116
|
-
"default_grant_threw",
|
|
115
|
+
"not_maintainer",
|
|
117
116
|
"pool_not_declared",
|
|
118
|
-
"
|
|
119
|
-
"pool_grant_threw",
|
|
117
|
+
"not_pool_member",
|
|
120
118
|
]);
|
|
121
119
|
/** The registry's connector id grammar (src/registry.ts). */
|
|
122
120
|
const CONNECTOR_ID_RE = /^[a-z0-9_-]{1,64}$/;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { OperatorUiContractV2 } from "./contract.js";
|
|
2
|
+
import { type Notice, type RequestFailureFacts, type Tone } from "./view.js";
|
|
3
|
+
/**
|
|
4
|
+
* The Tokens page's rows, computed from the slot list and contract v2 alone.
|
|
5
|
+
* Agents are declared in code: a row's only controls are its token's issue,
|
|
6
|
+
* rotate and revoke, so nothing here creates, renames or rebinds one.
|
|
7
|
+
*/
|
|
8
|
+
/** One declared agent's token slot, as `GET /ui/access-tokens` returns it. */
|
|
9
|
+
export type AgentSlot = ({
|
|
10
|
+
name: string;
|
|
11
|
+
state: "none";
|
|
12
|
+
} | {
|
|
13
|
+
name: string;
|
|
14
|
+
state: "issued";
|
|
15
|
+
issuedAt: string;
|
|
16
|
+
} | {
|
|
17
|
+
name: string;
|
|
18
|
+
state: "revoked";
|
|
19
|
+
revokedAt: string;
|
|
20
|
+
}) & {
|
|
21
|
+
/**
|
|
22
|
+
* The route's opaque slot revision; it changes on every slot commit (P3b,
|
|
23
|
+
* #837). A slot read without one is unresolved and never acted on.
|
|
24
|
+
*/
|
|
25
|
+
revision?: string;
|
|
26
|
+
};
|
|
27
|
+
/** Issue and rotate are the same request; the slot's state names it. */
|
|
28
|
+
export type SlotAction = "issue" | "rotate" | "revoke";
|
|
29
|
+
export interface AgentRow {
|
|
30
|
+
name: string;
|
|
31
|
+
/** Pools it belongs to, from `agents[].pools`; empty when the contract does not list it. */
|
|
32
|
+
pools: string[];
|
|
33
|
+
slot: AgentSlot;
|
|
34
|
+
/**
|
|
35
|
+
* Whether `slot` comes from the live slot list. Only a live slot can be
|
|
36
|
+
* acted on: one known only from the contract may be stale, so its controls
|
|
37
|
+
* are disabled until a refresh lists it.
|
|
38
|
+
*/
|
|
39
|
+
live: boolean;
|
|
40
|
+
/** Why its controls are off, or null when they work. */
|
|
41
|
+
blocked: string | null;
|
|
42
|
+
/** Its latest activity row; null is "never" (or history that is off). */
|
|
43
|
+
lastSeenAt: string | null;
|
|
44
|
+
actions: SlotAction[];
|
|
45
|
+
}
|
|
46
|
+
export declare function slotActions(slot: Pick<AgentSlot, "state">): SlotAction[];
|
|
47
|
+
/**
|
|
48
|
+
* One agent's slot, resolved once for both what the row shows and what an
|
|
49
|
+
* action may do. The live slot list is authoritative; the contract's copy is
|
|
50
|
+
* shown only for an agent the list does not have, and is never acted on.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveSlot(contract: OperatorUiContractV2 | null, slots: AgentSlot[], name: string): {
|
|
53
|
+
slot: AgentSlot;
|
|
54
|
+
live: boolean;
|
|
55
|
+
} | null;
|
|
56
|
+
/**
|
|
57
|
+
* One row per declared agent, in the slot list's order, then any agent only
|
|
58
|
+
* the contract lists. The contract adds pools and last seen.
|
|
59
|
+
*/
|
|
60
|
+
export declare function agentRows(contract: OperatorUiContractV2 | null, slots: AgentSlot[], unresolved?: readonly string[]): AgentRow[];
|
|
61
|
+
export declare function slotBadge(slot: Pick<AgentSlot, "state">): {
|
|
62
|
+
label: string;
|
|
63
|
+
tone: Tone;
|
|
64
|
+
};
|
|
65
|
+
/** When the slot last changed, as "Issued <date>" or "Revoked <date>"; a dash when it never has. */
|
|
66
|
+
export declare function slotSince(slot: AgentSlot, format: (value: string) => string): string;
|
|
67
|
+
export declare function lastSeenLabel(lastSeenAt: string | null, format: (value: string) => string): string;
|
|
68
|
+
/**
|
|
69
|
+
* Why a token operation did not land, in fixed sentences keyed by status.
|
|
70
|
+
* `outcome_unknown` (503) and `aborted` (499) are failures like any other and
|
|
71
|
+
* are never retried for the operator: an unknown rotation returned no secret,
|
|
72
|
+
* and only a new, deliberate rotation produces one.
|
|
73
|
+
*/
|
|
74
|
+
export declare function tokenFailureNotice(action: SlotAction, name: string, facts: RequestFailureFacts, productName: string): Notice;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { failure } from "./view.js";
|
|
2
|
+
/** Why a row's controls are disabled when its slot is not in the live list. */
|
|
3
|
+
const SLOT_UNAVAILABLE = "Token state unavailable, refresh";
|
|
4
|
+
/** Why a row's controls are disabled until a fresh list read resolves its slot. */
|
|
5
|
+
const SLOT_UNRESOLVED = "Refresh to continue";
|
|
6
|
+
export function slotActions(slot) {
|
|
7
|
+
return slot.state === "issued" ? ["rotate", "revoke"] : ["issue"];
|
|
8
|
+
}
|
|
9
|
+
function contractSlot(agent) {
|
|
10
|
+
return { name: agent.name, ...agent.token };
|
|
11
|
+
}
|
|
12
|
+
function maintainerAgents(contract) {
|
|
13
|
+
return contract?.role === "maintainer" ? contract.agents : [];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* One agent's slot, resolved once for both what the row shows and what an
|
|
17
|
+
* action may do. The live slot list is authoritative; the contract's copy is
|
|
18
|
+
* shown only for an agent the list does not have, and is never acted on.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveSlot(contract, slots, name) {
|
|
21
|
+
const listed = slots.find((slot) => slot.name === name);
|
|
22
|
+
if (listed)
|
|
23
|
+
return { slot: listed, live: true };
|
|
24
|
+
const agent = maintainerAgents(contract).find((candidate) => candidate.name === name);
|
|
25
|
+
return agent ? { slot: contractSlot(agent), live: false } : null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* One row per declared agent, in the slot list's order, then any agent only
|
|
29
|
+
* the contract lists. The contract adds pools and last seen.
|
|
30
|
+
*/
|
|
31
|
+
export function agentRows(contract, slots, unresolved = []) {
|
|
32
|
+
const agents = maintainerAgents(contract);
|
|
33
|
+
const declared = new Map(agents.map((agent) => [agent.name, agent]));
|
|
34
|
+
const names = [...slots.map((slot) => slot.name), ...agents.map((agent) => agent.name)];
|
|
35
|
+
return [...new Set(names)].map((name) => {
|
|
36
|
+
const { slot, live } = resolveSlot(contract, slots, name);
|
|
37
|
+
const agent = declared.get(name);
|
|
38
|
+
return {
|
|
39
|
+
name,
|
|
40
|
+
pools: agent?.pools ?? [],
|
|
41
|
+
slot,
|
|
42
|
+
live,
|
|
43
|
+
blocked: !live ? SLOT_UNAVAILABLE : unresolved.includes(name) || !slot.revision ? SLOT_UNRESOLVED : null,
|
|
44
|
+
lastSeenAt: agent?.lastSeenAt ?? null,
|
|
45
|
+
actions: slotActions(slot),
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function slotBadge(slot) {
|
|
50
|
+
if (slot.state === "issued")
|
|
51
|
+
return { label: "Issued", tone: "ok" };
|
|
52
|
+
if (slot.state === "revoked")
|
|
53
|
+
return { label: "Revoked", tone: "warn" };
|
|
54
|
+
return { label: "No token", tone: "neutral" };
|
|
55
|
+
}
|
|
56
|
+
/** When the slot last changed, as "Issued <date>" or "Revoked <date>"; a dash when it never has. */
|
|
57
|
+
export function slotSince(slot, format) {
|
|
58
|
+
if (slot.state === "issued")
|
|
59
|
+
return `Issued ${format(slot.issuedAt)}`;
|
|
60
|
+
if (slot.state === "revoked")
|
|
61
|
+
return `Revoked ${format(slot.revokedAt)}`;
|
|
62
|
+
return "—";
|
|
63
|
+
}
|
|
64
|
+
export function lastSeenLabel(lastSeenAt, format) {
|
|
65
|
+
return lastSeenAt ? format(lastSeenAt) : "never";
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Why a token operation did not land, in fixed sentences keyed by status.
|
|
69
|
+
* `outcome_unknown` (503) and `aborted` (499) are failures like any other and
|
|
70
|
+
* are never retried for the operator: an unknown rotation returned no secret,
|
|
71
|
+
* and only a new, deliberate rotation produces one.
|
|
72
|
+
*/
|
|
73
|
+
export function tokenFailureNotice(action, name, facts, productName) {
|
|
74
|
+
if (facts.kind === "session")
|
|
75
|
+
return failure("Your session has ended. Sign in again, then retry.");
|
|
76
|
+
if (facts.kind === "forbidden")
|
|
77
|
+
return failure("You don't have permission to manage agent tokens.");
|
|
78
|
+
if (facts.kind === "stale") {
|
|
79
|
+
return failure(`${name}'s token changed before the request was sent, so nothing was sent. Check it and try again.`);
|
|
80
|
+
}
|
|
81
|
+
if (facts.kind === "network") {
|
|
82
|
+
return failure(`Couldn't reach ${productName}. Refresh to check ${name}'s token before trying again.`);
|
|
83
|
+
}
|
|
84
|
+
if (facts.status === 503) {
|
|
85
|
+
return failure(action === "revoke"
|
|
86
|
+
? `Revocation outcome unknown for ${name}: revoke again to be sure its token is stopped.`
|
|
87
|
+
: `${action === "issue" ? "Issue" : "Rotation"} outcome unknown for ${name}: no secret was returned, so rotate again for a replacement.`);
|
|
88
|
+
}
|
|
89
|
+
if (facts.status === 499) {
|
|
90
|
+
return failure(action === "revoke"
|
|
91
|
+
? `The request ended before ${name}'s revocation finished. Refresh to check its token before trying again.`
|
|
92
|
+
: `The request ended before ${name}'s token was delivered, so no secret was returned. Refresh to check its token before trying again.`);
|
|
93
|
+
}
|
|
94
|
+
// The route's compare-and-swap refused it: the slot moved after the list was read.
|
|
95
|
+
if (facts.status === 409 && facts.code === "slot_changed")
|
|
96
|
+
return failure("Changed since you looked, refresh.");
|
|
97
|
+
if (facts.status === 409)
|
|
98
|
+
return failure(`${name}'s token changed meanwhile. Reload and try again.`);
|
|
99
|
+
if (facts.status === 404)
|
|
100
|
+
return failure(`${name} is no longer declared in code. Reload the page.`);
|
|
101
|
+
return action === "revoke"
|
|
102
|
+
? failure(`${name}'s token could not be revoked. Reload and try again.`)
|
|
103
|
+
: failure(`A token could not be issued for ${name}. Reload and try again.`);
|
|
104
|
+
}
|