@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.
Files changed (94) hide show
  1. package/AGENTS.md +5 -0
  2. package/CHANGELOG.md +82 -0
  3. package/README.md +19 -11
  4. package/dist/activity-friction.d.ts +3 -0
  5. package/dist/activity-friction.js +19 -0
  6. package/dist/activity.d.ts +11 -2
  7. package/dist/activity.js +15 -19
  8. package/dist/auth/downstream-oauth.d.ts +2 -1
  9. package/dist/auth/downstream-oauth.js +10 -1
  10. package/dist/branding.d.ts +67 -0
  11. package/dist/branding.js +176 -0
  12. package/dist/catalog-service.d.ts +1 -6
  13. package/dist/catalog-service.js +3 -53
  14. package/dist/connectors/remote-mcp.js +3 -5
  15. package/dist/credential-contract.d.ts +24 -0
  16. package/dist/credential-contract.js +1 -0
  17. package/dist/credential-rules.d.ts +85 -0
  18. package/dist/credential-rules.js +107 -0
  19. package/dist/credentials.d.ts +4 -100
  20. package/dist/credentials.js +3 -107
  21. package/dist/errors.d.ts +1 -1
  22. package/dist/execute.d.ts +5 -52
  23. package/dist/execute.js +40 -347
  24. package/dist/executors/quickjs-protocol.d.ts +0 -7
  25. package/dist/executors/quickjs-protocol.js +2 -10
  26. package/dist/executors/quickjs.js +1 -1
  27. package/dist/index.d.ts +23 -56
  28. package/dist/index.js +30 -58
  29. package/dist/invocation.d.ts +0 -33
  30. package/dist/invocation.js +56 -124
  31. package/dist/meta-tools.d.ts +7 -6
  32. package/dist/meta-tools.js +14 -21
  33. package/dist/module-contracts.d.ts +19 -0
  34. package/dist/module-contracts.js +1 -0
  35. package/dist/operator-ui/generated.js +2 -2
  36. package/dist/operator-ui/model.d.ts +6 -3
  37. package/dist/operator-ui/view.d.ts +2 -18
  38. package/dist/operator-ui/view.js +3 -20
  39. package/dist/registry.d.ts +4 -1
  40. package/dist/registry.js +8 -6
  41. package/dist/routes/activity.js +1 -1
  42. package/dist/routes/credentials.js +5 -2
  43. package/dist/routes/mcp.js +7 -51
  44. package/dist/routes/oauth-management.d.ts +2 -0
  45. package/dist/routes/oauth-management.js +108 -0
  46. package/dist/routes/oauth.d.ts +0 -1
  47. package/dist/routes/oauth.js +21 -121
  48. package/dist/routes/shared.d.ts +19 -17
  49. package/dist/routes/shared.js +48 -44
  50. package/dist/routes/ui.js +36 -33
  51. package/dist/server.d.ts +1 -2
  52. package/dist/server.js +7 -45
  53. package/dist/skills.d.ts +1 -1
  54. package/dist/skills.js +55 -19
  55. package/dist/types.d.ts +3 -15
  56. package/dist/ui.d.ts +15 -70
  57. package/dist/ui.js +176 -317
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. package/documentation/architecture.md +31 -26
  61. package/documentation/auth.md +65 -111
  62. package/documentation/call-admission.md +10 -11
  63. package/documentation/cloudflare.md +1 -1
  64. package/documentation/code-mode.md +65 -260
  65. package/documentation/connectors.md +8 -6
  66. package/documentation/linear.md +1 -1
  67. package/documentation/meta-tools.md +37 -18
  68. package/documentation/mixpanel.md +1 -1
  69. package/documentation/notion.md +2 -2
  70. package/documentation/operations.md +19 -23
  71. package/documentation/operator-ui.md +82 -104
  72. package/documentation/optional-modules-upgrade.md +243 -0
  73. package/documentation/provider-conventions.md +7 -5
  74. package/documentation/revenuecat.md +1 -1
  75. package/documentation/storage-and-credentials.md +59 -40
  76. package/documentation/stripe.md +1 -1
  77. package/documentation/upgrading.md +83 -8
  78. package/ethos.md +36 -43
  79. package/examples/worker/AGENTS.md +3 -1
  80. package/examples/worker/README.md +68 -84
  81. package/examples/worker/src/d1-activity.ts +1 -1
  82. package/examples/worker/src/index.ts +11 -6
  83. package/package.json +18 -2
  84. package/templates/node/AGENTS.md +8 -6
  85. package/templates/node/README.md +56 -67
  86. package/templates/node/package.json +1 -1
  87. package/templates/node/src/file-activity.ts +1 -1
  88. package/templates/node/src/index.ts +11 -12
  89. package/dist/access-tokens.d.ts +0 -31
  90. package/dist/access-tokens.js +0 -236
  91. package/dist/apps-shell.d.ts +0 -37
  92. package/dist/apps-shell.js +0 -174
  93. package/dist/routes/access-tokens.d.ts +0 -6
  94. package/dist/routes/access-tokens.js +0 -83
@@ -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
- ## Turn on the operator surface
47
+ ## Select optional modules
48
48
 
49
- Out of the box this deployment serves the seven-tool MCP surface and a
50
- read-only operator UI: open `http://localhost:8787/`, paste the bearer, and you
51
- get Connections. The other three pages are configuration away, and each one is
52
- a commented block in `src/index.ts` uncomment it, set the variables it names
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
- **1. Operator sign-in (Clerk).** A bearer token is a client key. It may call
57
- tools and read connector status, but it may not write a credential or issue an
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 # optional peer; it does not install with Connecta
58
+ npm install @clerk/backend
63
59
  ```
64
60
 
65
- Set `CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY`, uncomment the `clerkAuth`
66
- import, the two `process.env.CLERK_*` reads, and the `clerkAuth({ … })` entry in
67
- `auth`. Enable Dynamic Client Registration on the Clerk instance (OAuth
68
- Applications DCR) if MCP clients should sign in through it too, and set
69
- `PUBLIC_URL` first Clerk redirects back to it.
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
- Clerk remains the identity provider when several people share this Docker
72
- deployment. Uncomment the `identity` block in `src/index.ts` to give each Clerk
73
- principal a config-derived connector view and to choose operators. Add
74
- `authScope: "personal"` to a connector when each person should supply their own
75
- credential or finish their own downstream OAuth flow. Without those options,
76
- all connectors and auth stay shared exactly as before.
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
- **2. Credential vault.** Uncomment `credentials` and set
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
- Every connector that declares a `credential` slot then becomes editable at
86
- `/credentials`, with values encrypted in the state file. The shipped `time`
87
- connector declares none telling the time needs no secret so the key alone
88
- leaves the page hidden, which is the honest state for a page with nothing on
89
- it. Add `credential: { label: "API token" }` to an `api()` connector (the
90
- commented shape is in `src/index.ts`) and read it in a handler with
91
- `await ctx.credential?.get()`, or use a provider connector such as `notion()`,
92
- which declares its own; Credentials appears for a signed-in operator on the
93
- next restart. Keep the key anywhere
94
- except that file it is the only thing standing between a copied state file
95
- and the secrets in it — and note that losing it makes stored values
96
- unreadable. A saved replacement takes effect on the next call; nothing
97
- restarts, and the deployment never probes a credential to see whether it still
98
- works. It fails at use, loudly, and the agent is routed to `/credentials`.
99
-
100
- **3. Access tokens.** Uncomment `accessTokens: {}`. A signed-in operator can
101
- then mint named, revocable Bearer tokens at `/tokens` for header-capable
102
- clients that will not do OAuth. Secrets are shown once and only their hashes
103
- are stored, so a lost token is reissued, never recovered.
104
-
105
- **4. Activity.** Uncomment the `activity` block and the `fileActivityStore`
106
- import. `/activity` then answers with who called what, when, how long it took,
107
- and whether it worked — never arguments, results, generated code, or raw error
108
- messages, because the store is never handed one. `src/file-activity.ts` is
109
- yours: it appends a line per call and rewrites the log down to the newest 5,000
110
- events once it runs a slack window past that, so the file holds a few hundred
111
- more than the ceiling between rewrites rather than being rewritten on every
112
- call. That is a retention policy chosen for a single container and worth
113
- revisiting for anything busier. In Docker the log lands on the same volume as
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
- That is deliberate. Doctor holds a bearer, and a bearer learns the model-facing
157
- surface, not the deployment's configuration topology: whether this deployment
158
- issues access tokens or keeps a credential vault is operator data, and a client
159
- key is not an operator. Confirm the operator surface the way an operator will —
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.
@@ -15,7 +15,7 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "@zackbart/connecta": "0.22.3",
18
+ "@zackbart/connecta": "0.24.0",
19
19
  "quickjs-emscripten": "0.32.0"
20
20
  },
21
21
  "devDependencies": {
@@ -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, bearerToken, createConnecta } from "@zackbart/connecta";
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
- // operatorAccess: ({ id }) => id === "user_admin",
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
- // credentials: { encryptionKey: process.env.CONNECTA_CREDENTIAL_KEY },
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",
@@ -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
- }
@@ -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
- }
@@ -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";