@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/templates/node/README.md
CHANGED
|
@@ -44,79 +44,72 @@ So commit the `package-lock.json` that the `npm install` above wrote on this
|
|
|
44
44
|
machine: from then on the build context carries it and every build takes the
|
|
45
45
|
reproducible `npm ci` path.
|
|
46
46
|
|
|
47
|
-
##
|
|
47
|
+
## Select optional modules
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
in `.env`, restart. Do them in this order; the last two lean on the first, and
|
|
54
|
-
Credentials wants one thing more than a block, called out in step 2.
|
|
49
|
+
The template explicitly enables `ui: operatorUi()` from
|
|
50
|
+
`@zackbart/connecta/ui`. Open `http://localhost:8787/` and supply the configured
|
|
51
|
+
bearer to inspect Connections. Omit that option and import for an API-only
|
|
52
|
+
server. OAuth callbacks remain in core even with no UI.
|
|
55
53
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
access token — that would make one shared secret a deployment-admin key. An
|
|
59
|
-
interactive identity is what unlocks the actionable half:
|
|
54
|
+
Connection management needs an interactive identity. A configured bearer is a
|
|
55
|
+
client key and never authorizes browser credential mutations. To enable Clerk:
|
|
60
56
|
|
|
61
57
|
```sh
|
|
62
|
-
npm install @clerk/backend
|
|
58
|
+
npm install @clerk/backend
|
|
63
59
|
```
|
|
64
60
|
|
|
65
|
-
Set `CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY`,
|
|
66
|
-
import
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
Set `CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY`, enable the corresponding
|
|
62
|
+
`clerkAuth` import and auth entry in `src/index.ts`, and set `PUBLIC_URL`.
|
|
63
|
+
Enable Dynamic Client Registration on the Clerk instance if MCP clients should
|
|
64
|
+
sign in with OAuth. Connecta no longer issues named client access tokens; keep
|
|
65
|
+
the configured bearer only for clients that need it.
|
|
70
66
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
`authScope: "personal"`
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
Set the code-owned identity resolvers deliberately. `connectorAccess` governs
|
|
68
|
+
use; `credentialAdministration` permits shared-auth changes, and
|
|
69
|
+
`personalConnection` permits the signed-in principal's personal-auth changes.
|
|
70
|
+
Both management permissions default to none. Use `authScope: "personal"` for a
|
|
71
|
+
connector where each person should connect their own downstream account.
|
|
72
|
+
`activityAccess` separately selects readers of global activity.
|
|
77
73
|
|
|
78
|
-
|
|
74
|
+
### Credential vault
|
|
75
|
+
|
|
76
|
+
Import `encryptedCredentialVault` from `@zackbart/connecta/credentials`, then
|
|
77
|
+
set `vault: encryptedCredentialVault(storage, credentialKey)`. Set
|
|
79
78
|
`CONNECTA_CREDENTIAL_KEY` to a base64 32-byte AES key:
|
|
80
79
|
|
|
81
80
|
```sh
|
|
82
81
|
node -e "console.log(crypto.randomBytes(32).toString('base64'))"
|
|
83
82
|
```
|
|
84
83
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
the state file.
|
|
115
|
-
|
|
116
|
-
None of this changes what agents can reach. Operator pages manage the
|
|
117
|
-
authentication material behind capabilities this file already declares; the
|
|
118
|
-
connector set, the tool catalog, and its annotations are `src/index.ts`'s
|
|
119
|
-
business and stay that way.
|
|
84
|
+
Keep this key outside the state file. Losing it makes saved values unreadable;
|
|
85
|
+
upgrades must reuse it. The shipped `time` connector declares no credential
|
|
86
|
+
slot. Add `credential: { label: "API token" }` to an `api()` connector and read
|
|
87
|
+
it through `await ctx.credential?.get()`, or use a provider such as `notion()`
|
|
88
|
+
that declares its own slot. Authorized humans manage the slot inside that
|
|
89
|
+
connection on `/`; there is no separate Credentials tab.
|
|
90
|
+
|
|
91
|
+
A saved replacement takes effect on the next call. Connecta tests credentials
|
|
92
|
+
only on an explicit action and otherwise fails at use. Without a vault or UI,
|
|
93
|
+
static credential recovery reports unavailable instead of offering a dead link.
|
|
94
|
+
|
|
95
|
+
### Activity history and diagnostics
|
|
96
|
+
|
|
97
|
+
Import `activityHistory` from `@zackbart/connecta/activity` and wire the template's
|
|
98
|
+
`fileActivityStore` through `activity: activityHistory({ store })`. The Activity
|
|
99
|
+
tab appears for authorized readers when the store supports listing. Omit this
|
|
100
|
+
option and its store wiring to record no activity.
|
|
101
|
+
|
|
102
|
+
`src/file-activity.ts` belongs to the deployment. It appends payload-free events
|
|
103
|
+
and periodically retains the newest 5,000, allowing a small slack window between
|
|
104
|
+
rewrites. Docker stores the log on the state volume. It records no arguments,
|
|
105
|
+
results, generated code, or raw errors. Adjust retention in that file if needed.
|
|
106
|
+
|
|
107
|
+
Diagnostics are independent. Keep the default logger or provide your own;
|
|
108
|
+
`logger: "silent"` suppresses diagnostic output explicitly.
|
|
109
|
+
|
|
110
|
+
The UI displays connections and current permissions. Configuration still owns
|
|
111
|
+
the connector set, tool definitions, and access rules. There is no token tab,
|
|
112
|
+
team roster, or policy editor.
|
|
120
113
|
|
|
121
114
|
## Deployment contract
|
|
122
115
|
|
|
@@ -153,11 +146,7 @@ Connecta doctor passed: 1 connector(s), QuickJS executed, prescribed seven-tool
|
|
|
153
146
|
`QuickJS` is this deployment's sandbox, reported by the deployment itself —
|
|
154
147
|
swap the executor and doctor names the one that actually ran the program.
|
|
155
148
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
sign in at `/` and check that the pages you turned on are there: Tokens and
|
|
161
|
-
Activity once their blocks are uncommented, and Credentials once the vault has
|
|
162
|
-
a connector credential slot to show. The nav lists a page only when this
|
|
163
|
-
deployment can serve it, so an absent page is a report, not a fault.
|
|
149
|
+
Doctor verifies the MCP contract. Verify UI behavior separately: sign in at `/`,
|
|
150
|
+
confirm the visible connections and their permitted auth controls, and check
|
|
151
|
+
Activity only when you enabled a readable history store. A missing optional
|
|
152
|
+
feature should not leave a tab behind.
|
|
@@ -27,7 +27,7 @@ import type {
|
|
|
27
27
|
ActivityStore,
|
|
28
28
|
ToolCallActivityEvent,
|
|
29
29
|
} from "@zackbart/connecta";
|
|
30
|
-
import { InvalidActivityCursorError } from "@zackbart/connecta";
|
|
30
|
+
import { InvalidActivityCursorError } from "@zackbart/connecta/activity";
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Events allowed past `maxEvents` before the log is rewritten. Trimming on the
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { bearerToken } from "@zackbart/connecta/auth/bearer";
|
|
2
|
+
import { operatorUi } from "@zackbart/connecta/ui";
|
|
3
|
+
// import { encryptedCredentialVault } from "@zackbart/connecta/credentials";
|
|
4
|
+
// import { activityHistory } from "@zackbart/connecta/activity";
|
|
1
5
|
/**
|
|
2
6
|
* Prescribed Connecta deployment.
|
|
3
7
|
*
|
|
@@ -20,7 +24,7 @@
|
|
|
20
24
|
* CONNECTA_CREDENTIAL_KEY vault key, once the credentials block is on
|
|
21
25
|
* CONNECTA_ACTIVITY_FILE activity log, once the activity block is on
|
|
22
26
|
*/
|
|
23
|
-
import { api,
|
|
27
|
+
import { api, createConnecta } from "@zackbart/connecta";
|
|
24
28
|
import { fileStorage, listen } from "@zackbart/connecta/node";
|
|
25
29
|
import { quickJsExecutor } from "@zackbart/connecta/quickjs";
|
|
26
30
|
// Operator sign-in. Needs `npm install @clerk/backend` — it is an optional
|
|
@@ -74,7 +78,7 @@ const connecta = createConnecta({
|
|
|
74
78
|
// identity: {
|
|
75
79
|
// connectorAccess: ({ principal }) =>
|
|
76
80
|
// principal?.id === "user_admin" ? "all" : ["time"],
|
|
77
|
-
//
|
|
81
|
+
// activityAccess: ({ id }) => id === "user_admin",
|
|
78
82
|
// },
|
|
79
83
|
publicUrl,
|
|
80
84
|
// Required: model-written programs run in a bounded QuickJS child.
|
|
@@ -84,24 +88,19 @@ const connecta = createConnecta({
|
|
|
84
88
|
// with this key — so keep the key out of that file and out of source:
|
|
85
89
|
// node -e "console.log(crypto.randomBytes(32).toString('base64'))"
|
|
86
90
|
// Rotating a credential takes effect on the next call; no restart.
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
// Named, revocable Bearer tokens for MCP clients, issued at /tokens by a
|
|
90
|
-
// signed-in operator. Secrets are shown once; only their hashes are stored.
|
|
91
|
-
// Requires the Clerk block above — there is nobody to authorize issuance
|
|
92
|
-
// otherwise.
|
|
93
|
-
// accessTokens: {},
|
|
94
|
-
//
|
|
91
|
+
// vault: encryptedCredentialVault(storage, process.env.CONNECTA_CREDENTIAL_KEY!),
|
|
95
92
|
// Payload-free activity history at /activity: who called what, when, how
|
|
96
93
|
// long it took, and whether it worked. Never arguments, results, generated
|
|
97
94
|
// code, or raw error messages. Commented because retention is yours to
|
|
98
95
|
// choose — see src/file-activity.ts.
|
|
99
|
-
// activity: {
|
|
96
|
+
// activity: activityHistory({
|
|
100
97
|
// store: fileActivityStore(
|
|
101
98
|
// process.env.CONNECTA_ACTIVITY_FILE || "./.connecta-activity.jsonl",
|
|
102
99
|
// ),
|
|
103
100
|
// deploymentId: "production",
|
|
104
|
-
// },
|
|
101
|
+
// }),
|
|
102
|
+
ui: operatorUi(),
|
|
103
|
+
identity: { credentialAdministration: () => "all", personalConnection: () => "all" },
|
|
105
104
|
connectors: [
|
|
106
105
|
api("time", {
|
|
107
106
|
description: "Time — current timestamp",
|
package/dist/access-tokens.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { IdentityReference, InboundAuth, KVStorage } from "./types.js";
|
|
2
|
-
export interface AccessTokenMetadata {
|
|
3
|
-
id: string;
|
|
4
|
-
name: string;
|
|
5
|
-
tokenPrefix: string;
|
|
6
|
-
createdAt: string;
|
|
7
|
-
revokedAt?: string;
|
|
8
|
-
}
|
|
9
|
-
export interface CreatedAccessToken {
|
|
10
|
-
token: string;
|
|
11
|
-
accessToken: AccessTokenMetadata;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Deployment-scoped personal access tokens. Secret material is never
|
|
15
|
-
* recoverable: authentication indexes a SHA-256 digest of a random 256-bit
|
|
16
|
-
* token, while separately enumerable metadata powers operator management.
|
|
17
|
-
*/
|
|
18
|
-
export declare class AccessTokenManager {
|
|
19
|
-
private readonly storage;
|
|
20
|
-
readonly auth: InboundAuth;
|
|
21
|
-
private readonly maxActive;
|
|
22
|
-
constructor(storage: KVStorage, options?: {
|
|
23
|
-
maxActive?: number;
|
|
24
|
-
});
|
|
25
|
-
private read;
|
|
26
|
-
list(): Promise<AccessTokenMetadata[]>;
|
|
27
|
-
create(name: unknown, createdBy: string | IdentityReference): Promise<CreatedAccessToken>;
|
|
28
|
-
rename(id: string, name: unknown): Promise<AccessTokenMetadata | null>;
|
|
29
|
-
revoke(id: string, revokedBy: string): Promise<AccessTokenMetadata | null>;
|
|
30
|
-
private authorize;
|
|
31
|
-
}
|
package/dist/access-tokens.js
DELETED
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import { validIdentityReference } from "./identity.js";
|
|
2
|
-
const TOKEN_PREFIX = "cta_";
|
|
3
|
-
const TOKEN_BYTES = 32;
|
|
4
|
-
const TOKEN_VALUE_RE = /^cta_[A-Za-z0-9_-]{43}$/;
|
|
5
|
-
const RECORD_PREFIX = "access-token:v1:record:";
|
|
6
|
-
const LOOKUP_PREFIX = "access-token:v1:lookup:";
|
|
7
|
-
const MAX_NAME_CHARACTERS = 80;
|
|
8
|
-
const DEFAULT_MAX_ACTIVE = 100;
|
|
9
|
-
const MAX_CONFIGURED_ACTIVE = 1_000;
|
|
10
|
-
const encoder = new TextEncoder();
|
|
11
|
-
function recordKey(id) {
|
|
12
|
-
return `${RECORD_PREFIX}${id}`;
|
|
13
|
-
}
|
|
14
|
-
function lookupKey(hash) {
|
|
15
|
-
return `${LOOKUP_PREFIX}${hash}`;
|
|
16
|
-
}
|
|
17
|
-
function bytesToBase64Url(bytes) {
|
|
18
|
-
let binary = "";
|
|
19
|
-
for (const byte of bytes)
|
|
20
|
-
binary += String.fromCharCode(byte);
|
|
21
|
-
return btoa(binary)
|
|
22
|
-
.replaceAll("+", "-")
|
|
23
|
-
.replaceAll("/", "_")
|
|
24
|
-
.replace(/=+$/u, "");
|
|
25
|
-
}
|
|
26
|
-
function bytesToHex(bytes) {
|
|
27
|
-
return [...bytes]
|
|
28
|
-
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
29
|
-
.join("");
|
|
30
|
-
}
|
|
31
|
-
async function hashToken(token) {
|
|
32
|
-
return bytesToHex(new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(token))));
|
|
33
|
-
}
|
|
34
|
-
function normalizeName(value) {
|
|
35
|
-
if (typeof value !== "string") {
|
|
36
|
-
throw new Error("Token name must be a string");
|
|
37
|
-
}
|
|
38
|
-
const compact = value.replace(/\s+/gu, " ").trim();
|
|
39
|
-
if (!compact)
|
|
40
|
-
throw new Error("Token name cannot be empty");
|
|
41
|
-
if (Array.from(compact).length > MAX_NAME_CHARACTERS) {
|
|
42
|
-
throw new Error(`Token name cannot exceed ${MAX_NAME_CHARACTERS} characters`);
|
|
43
|
-
}
|
|
44
|
-
return compact;
|
|
45
|
-
}
|
|
46
|
-
function parseRecord(raw) {
|
|
47
|
-
try {
|
|
48
|
-
const value = JSON.parse(raw);
|
|
49
|
-
if (value.version !== 1 ||
|
|
50
|
-
typeof value.id !== "string" ||
|
|
51
|
-
!/^[0-9a-f-]{36}$/u.test(value.id) ||
|
|
52
|
-
typeof value.name !== "string" ||
|
|
53
|
-
typeof value.tokenHash !== "string" ||
|
|
54
|
-
!/^[0-9a-f]{64}$/u.test(value.tokenHash) ||
|
|
55
|
-
typeof value.tokenPrefix !== "string" ||
|
|
56
|
-
typeof value.createdAt !== "string" ||
|
|
57
|
-
typeof value.createdBy !== "string" ||
|
|
58
|
-
(value.principal !== undefined &&
|
|
59
|
-
!validIdentityReference(value.principal)) ||
|
|
60
|
-
(value.revokedAt !== undefined &&
|
|
61
|
-
typeof value.revokedAt !== "string") ||
|
|
62
|
-
(value.revokedBy !== undefined &&
|
|
63
|
-
typeof value.revokedBy !== "string")) {
|
|
64
|
-
throw new Error("invalid token record");
|
|
65
|
-
}
|
|
66
|
-
return value;
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
throw new Error("Stored access token metadata is invalid or corrupted");
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
function parseLookup(raw) {
|
|
73
|
-
try {
|
|
74
|
-
const value = JSON.parse(raw);
|
|
75
|
-
return value.version === 1 && typeof value.id === "string"
|
|
76
|
-
? { version: 1, id: value.id }
|
|
77
|
-
: null;
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
function metadata(record) {
|
|
84
|
-
return {
|
|
85
|
-
id: record.id,
|
|
86
|
-
name: record.name,
|
|
87
|
-
tokenPrefix: record.tokenPrefix,
|
|
88
|
-
createdAt: record.createdAt,
|
|
89
|
-
...(record.revokedAt ? { revokedAt: record.revokedAt } : {}),
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
function unauthorized() {
|
|
93
|
-
return {
|
|
94
|
-
ok: false,
|
|
95
|
-
response: new Response(JSON.stringify({ error: "unauthorized" }), {
|
|
96
|
-
status: 401,
|
|
97
|
-
headers: {
|
|
98
|
-
"Content-Type": "application/json",
|
|
99
|
-
"WWW-Authenticate": "Bearer",
|
|
100
|
-
},
|
|
101
|
-
}),
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Deployment-scoped personal access tokens. Secret material is never
|
|
106
|
-
* recoverable: authentication indexes a SHA-256 digest of a random 256-bit
|
|
107
|
-
* token, while separately enumerable metadata powers operator management.
|
|
108
|
-
*/
|
|
109
|
-
export class AccessTokenManager {
|
|
110
|
-
storage;
|
|
111
|
-
auth;
|
|
112
|
-
maxActive;
|
|
113
|
-
constructor(storage, options = {}) {
|
|
114
|
-
this.storage = storage;
|
|
115
|
-
if (!storage.list) {
|
|
116
|
-
throw new Error("accessTokens requires a storage adapter that implements list(prefix)");
|
|
117
|
-
}
|
|
118
|
-
const maxActive = options.maxActive ?? DEFAULT_MAX_ACTIVE;
|
|
119
|
-
if (!Number.isInteger(maxActive) ||
|
|
120
|
-
maxActive < 1 ||
|
|
121
|
-
maxActive > MAX_CONFIGURED_ACTIVE) {
|
|
122
|
-
throw new Error(`accessTokens.maxActive must be a whole number from 1 to ${MAX_CONFIGURED_ACTIVE}`);
|
|
123
|
-
}
|
|
124
|
-
this.maxActive = maxActive;
|
|
125
|
-
this.auth = {
|
|
126
|
-
kind: "access_token",
|
|
127
|
-
activityActorNamespace: "connecta:access-tokens:v1",
|
|
128
|
-
activityActorLabel: async (id) => {
|
|
129
|
-
try {
|
|
130
|
-
return (await this.read(id))?.name;
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
return undefined;
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
authorize: (request) => this.authorize(request),
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
async read(id) {
|
|
140
|
-
const raw = await this.storage.get(recordKey(id));
|
|
141
|
-
return raw ? parseRecord(raw) : null;
|
|
142
|
-
}
|
|
143
|
-
async list() {
|
|
144
|
-
const keys = await this.storage.list(RECORD_PREFIX);
|
|
145
|
-
const records = await Promise.all(keys.map(async (key) => {
|
|
146
|
-
const raw = await this.storage.get(key);
|
|
147
|
-
return raw ? parseRecord(raw) : null;
|
|
148
|
-
}));
|
|
149
|
-
return records
|
|
150
|
-
.filter((record) => Boolean(record))
|
|
151
|
-
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
|
152
|
-
.map(metadata);
|
|
153
|
-
}
|
|
154
|
-
async create(name, createdBy) {
|
|
155
|
-
const normalizedName = normalizeName(name);
|
|
156
|
-
const active = (await this.list()).filter((token) => !token.revokedAt);
|
|
157
|
-
if (active.length >= this.maxActive) {
|
|
158
|
-
throw new Error(`This deployment already has the maximum of ${this.maxActive} active access tokens`);
|
|
159
|
-
}
|
|
160
|
-
const secretBytes = crypto.getRandomValues(new Uint8Array(TOKEN_BYTES));
|
|
161
|
-
const token = TOKEN_PREFIX + bytesToBase64Url(secretBytes);
|
|
162
|
-
const hash = await hashToken(token);
|
|
163
|
-
if (await this.storage.get(lookupKey(hash))) {
|
|
164
|
-
throw new Error("Access token collision; create another token");
|
|
165
|
-
}
|
|
166
|
-
const record = {
|
|
167
|
-
version: 1,
|
|
168
|
-
id: crypto.randomUUID(),
|
|
169
|
-
name: normalizedName,
|
|
170
|
-
tokenHash: hash,
|
|
171
|
-
tokenPrefix: token.slice(0, 12),
|
|
172
|
-
createdAt: new Date().toISOString(),
|
|
173
|
-
createdBy: typeof createdBy === "string"
|
|
174
|
-
? createdBy
|
|
175
|
-
: `${createdBy.namespace}:${createdBy.id}`,
|
|
176
|
-
...(typeof createdBy === "string"
|
|
177
|
-
? {}
|
|
178
|
-
: { principal: { ...createdBy } }),
|
|
179
|
-
};
|
|
180
|
-
await this.storage.set(recordKey(record.id), JSON.stringify(record));
|
|
181
|
-
try {
|
|
182
|
-
await this.storage.set(lookupKey(hash), JSON.stringify({ version: 1, id: record.id }));
|
|
183
|
-
}
|
|
184
|
-
catch (error) {
|
|
185
|
-
await this.storage.delete(recordKey(record.id)).catch(() => { });
|
|
186
|
-
throw error;
|
|
187
|
-
}
|
|
188
|
-
return { token, accessToken: metadata(record) };
|
|
189
|
-
}
|
|
190
|
-
async rename(id, name) {
|
|
191
|
-
const record = await this.read(id);
|
|
192
|
-
if (!record)
|
|
193
|
-
return null;
|
|
194
|
-
record.name = normalizeName(name);
|
|
195
|
-
await this.storage.set(recordKey(id), JSON.stringify(record));
|
|
196
|
-
return metadata(record);
|
|
197
|
-
}
|
|
198
|
-
async revoke(id, revokedBy) {
|
|
199
|
-
const record = await this.read(id);
|
|
200
|
-
if (!record)
|
|
201
|
-
return null;
|
|
202
|
-
if (!record.revokedAt) {
|
|
203
|
-
// Admission disappears first. A metadata-write failure may leave the UI
|
|
204
|
-
// calling the record active, but can never leave a token labelled
|
|
205
|
-
// revoked while its lookup still admits requests.
|
|
206
|
-
await this.storage.delete(lookupKey(record.tokenHash));
|
|
207
|
-
record.revokedAt = new Date().toISOString();
|
|
208
|
-
record.revokedBy = revokedBy;
|
|
209
|
-
await this.storage.set(recordKey(id), JSON.stringify(record));
|
|
210
|
-
}
|
|
211
|
-
return metadata(record);
|
|
212
|
-
}
|
|
213
|
-
async authorize(request) {
|
|
214
|
-
const header = request.headers.get("authorization") ?? "";
|
|
215
|
-
const match = /^Bearer\s+(.+)$/iu.exec(header);
|
|
216
|
-
const token = match?.[1];
|
|
217
|
-
if (!token || !TOKEN_VALUE_RE.test(token))
|
|
218
|
-
return unauthorized();
|
|
219
|
-
const hash = await hashToken(token);
|
|
220
|
-
const lookupRaw = await this.storage.get(lookupKey(hash));
|
|
221
|
-
if (!lookupRaw)
|
|
222
|
-
return unauthorized();
|
|
223
|
-
const lookup = parseLookup(lookupRaw);
|
|
224
|
-
if (!lookup)
|
|
225
|
-
return unauthorized();
|
|
226
|
-
const record = await this.read(lookup.id);
|
|
227
|
-
if (!record || record.revokedAt || record.tokenHash !== hash) {
|
|
228
|
-
return unauthorized();
|
|
229
|
-
}
|
|
230
|
-
return {
|
|
231
|
-
ok: true,
|
|
232
|
-
subjectId: record.id,
|
|
233
|
-
...(record.principal ? { principal: { ...record.principal } } : {}),
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
}
|
package/dist/apps-shell.d.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The one MCP Apps template connecta serves (`U5`, `U6`).
|
|
3
|
-
*
|
|
4
|
-
* A build-time string constant, not a file read at startup: the core is
|
|
5
|
-
* Web-API-only so it runs unchanged on Workers, and the same bytes have to
|
|
6
|
-
* serve everywhere. The shell is display-only: it renders whatever HTML a
|
|
7
|
-
* program handed `connecta.ui` inside a nested `srcdoc` frame and forwards no
|
|
8
|
-
* channel back from that frame to the host, so program-authored markup is
|
|
9
|
-
* inert beyond its own pixels.
|
|
10
|
-
*
|
|
11
|
-
* The address carries a version segment because hosts are permitted to
|
|
12
|
-
* prefetch and cache templates by URI: change these bytes, bump the version.
|
|
13
|
-
*/
|
|
14
|
-
/** The only `ui://` URI in the system. No program input reaches it. */
|
|
15
|
-
export declare const PROGRAM_UI_RESOURCE_URI = "ui://connecta/program-ui/v3";
|
|
16
|
-
/** The mimeType the Apps spec requires of an HTML template. */
|
|
17
|
-
export declare const PROGRAM_UI_MIME_TYPE = "text/html;profile=mcp-app";
|
|
18
|
-
/**
|
|
19
|
-
* The result `_meta` key carrying the payload (`U3`). A plain single-label
|
|
20
|
-
* prefix rather than the reverse-DNS form MCP's SHOULD prefers: connecta has
|
|
21
|
-
* no domain to reverse, and fabricating one to satisfy a SHOULD is a worse
|
|
22
|
-
* answer than the shape the key format's MUST already permits.
|
|
23
|
-
*/
|
|
24
|
-
export declare const PROGRAM_UI_META_KEY = "connecta/ui";
|
|
25
|
-
/** The one extension identifier connecta advertises (`U11`). */
|
|
26
|
-
export declare const MCP_APPS_EXTENSION = "io.modelcontextprotocol/ui";
|
|
27
|
-
/**
|
|
28
|
-
* The shell document. Dependency-free and deliberately small: it speaks the
|
|
29
|
-
* Apps postMessage dialect (`ui/initialize`, `ui/notifications/initialized`,
|
|
30
|
-
* `ui/notifications/tool-result`, `ui/notifications/size-changed`,
|
|
31
|
-
* `ui/resource-teardown`), lifts `_meta["connecta/ui"].html` out of the
|
|
32
|
-
* delivered tool result, and puts it in a frame. It declares no CSP domains,
|
|
33
|
-
* so the host applies its restrictive default and the `srcdoc` frame inherits
|
|
34
|
-
* `default-src 'none'`. The payload gets scripts and local interactivity, and
|
|
35
|
-
* no network.
|
|
36
|
-
*/
|
|
37
|
-
export declare const PROGRAM_UI_SHELL_HTML = "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>connecta program view</title>\n <style>\n html,\n body {\n margin: 0;\n padding: 0;\n background: transparent;\n }\n #program-view {\n display: block;\n width: 100%;\n min-height: 480px;\n border: 0;\n }\n </style>\n </head>\n <body>\n <iframe\n id=\"program-view\"\n title=\"Program-rendered view\"\n sandbox=\"allow-scripts\"\n srcdoc=\"\"\n ></iframe>\n <script>\n (function () {\n \"use strict\";\n // The host frame is the only peer this shell speaks to, in either\n // direction. The payload frame below is sandboxed to scripts alone,\n // with no same-origin escape, and is never handed a reply path:\n // anything it posts fails the source check and is dropped. There is\n // no bridge from program HTML to the host, by construction rather\n // than by validation.\n var host = window.parent;\n var view = document.getElementById(\"program-view\");\n var initializeId = \"connecta-ui-initialize\";\n var lastWidth = 0;\n var lastHeight = 0;\n\n function send(message) {\n if (!host || host === window) return;\n host.postMessage(message, \"*\");\n }\n\n function notify(method, params) {\n send({ jsonrpc: \"2.0\", method: method, params: params });\n }\n\n // Program views are fixed-height by construction. The shell has no\n // bridge to the payload frame \u2014 that is the security posture, not an\n // omission \u2014 so it can never learn the payload's content height, and\n // what it reports here is its own box: the min-height above, unless\n // the host has given it more. Taller content scrolls inside the inner\n // frame rather than growing the view. Raising the min-height is the\n // only lever; a content-height signal would cost the isolation.\n function reportSize() {\n var width = Math.ceil(document.documentElement.clientWidth);\n var height = Math.ceil(document.documentElement.scrollHeight);\n if (width === lastWidth && height === lastHeight) return;\n lastWidth = width;\n lastHeight = height;\n notify(\"ui/notifications/size-changed\", {\n width: width,\n height: height\n });\n }\n\n function payloadHtml(result) {\n if (!result || typeof result !== \"object\") return null;\n var meta = result._meta;\n if (!meta || typeof meta !== \"object\") return null;\n var payload = meta[\"connecta/ui\"];\n if (!payload || typeof payload !== \"object\") return null;\n var html = payload.html;\n return typeof html === \"string\" && html.length > 0 ? html : null;\n }\n\n function render(params) {\n var html =\n payloadHtml(params) ||\n payloadHtml(params && params.result) ||\n payloadHtml(params && params.toolResult);\n if (html === null) return;\n view.srcdoc = html;\n reportSize();\n }\n\n window.addEventListener(\"message\", function (event) {\n if (event.source !== host) return;\n var message = event.data;\n if (!message || message.jsonrpc !== \"2.0\") return;\n if (message.method === \"ui/notifications/tool-result\") {\n render(message.params);\n return;\n }\n if (message.method === \"ui/resource-teardown\") {\n // A host->view request, not a notification: the host waits for\n // this reply before it tears the view down. There is nothing to\n // release, so answer immediately rather than make it time out.\n if (message.id !== undefined && message.id !== null) {\n send({ jsonrpc: \"2.0\", id: message.id, result: {} });\n }\n return;\n }\n // Only a completed handshake earns \"initialized\". A JSON-RPC error\n // response carries the same id, and announcing initialization on one\n // would assert a handshake that never happened.\n if (message.id === initializeId && message.result !== undefined) {\n notify(\"ui/notifications/initialized\", {});\n }\n });\n\n window.addEventListener(\"resize\", reportSize);\n view.addEventListener(\"load\", reportSize);\n\n // Every field here is required by the Apps initialize schema, and a\n // conforming host rejects the request outright when one is missing \u2014\n // which would strand the shell before any tool result arrives.\n send({\n jsonrpc: \"2.0\",\n id: initializeId,\n method: \"ui/initialize\",\n params: {\n appInfo: { name: \"connecta program view\", version: \"1\" },\n appCapabilities: {},\n protocolVersion: \"2026-01-26\"\n }\n });\n reportSize();\n })();\n </script>\n </body>\n</html>\n";
|