@zackbart/connecta 0.23.0 → 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 +40 -0
- package/README.md +18 -10
- 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/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/index.d.ts +22 -55
- package/dist/index.js +30 -58
- package/dist/invocation.js +2 -3
- package/dist/meta-tools.d.ts +4 -0
- package/dist/meta-tools.js +8 -4
- 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 +4 -6
- package/dist/routes/activity.js +1 -1
- package/dist/routes/credentials.js +5 -2
- package/dist/routes/mcp.js +7 -3
- 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.js +6 -26
- package/dist/types.d.ts +2 -0
- 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 +26 -17
- package/documentation/auth.md +61 -106
- package/documentation/cloudflare.md +1 -1
- package/documentation/connectors.md +1 -1
- package/documentation/linear.md +1 -1
- package/documentation/meta-tools.md +6 -4
- package/documentation/mixpanel.md +1 -1
- package/documentation/notion.md +2 -2
- package/documentation/operations.md +12 -14
- package/documentation/operator-ui.md +82 -104
- package/documentation/optional-modules-upgrade.md +243 -0
- package/documentation/provider-conventions.md +5 -3
- package/documentation/revenuecat.md +1 -1
- package/documentation/storage-and-credentials.md +59 -40
- package/documentation/stripe.md +1 -1
- package/documentation/upgrading.md +29 -4
- package/ethos.md +22 -30
- 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/routes/access-tokens.d.ts +0 -6
- package/dist/routes/access-tokens.js +0 -83
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
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { type RouteContext } from "./shared.js";
|
|
2
|
-
/**
|
|
3
|
-
* Interactive-operator lifecycle for deployment access tokens. The token itself
|
|
4
|
-
* is deliberately never an administrator credential and cannot reach here.
|
|
5
|
-
*/
|
|
6
|
-
export declare function routeAccessTokens(context: RouteContext): Promise<Response | null>;
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { authorizeUiAdmin, isSameOrigin, msg, privateJson, } from "./shared.js";
|
|
2
|
-
async function readName(request) {
|
|
3
|
-
if (!request.headers
|
|
4
|
-
.get("content-type")
|
|
5
|
-
?.toLowerCase()
|
|
6
|
-
.startsWith("application/json")) {
|
|
7
|
-
return {
|
|
8
|
-
ok: false,
|
|
9
|
-
response: privateJson({ error: "Content-Type must be application/json" }, { status: 415 }),
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
const raw = await request.text();
|
|
13
|
-
if (raw.length > 1_000) {
|
|
14
|
-
return {
|
|
15
|
-
ok: false,
|
|
16
|
-
response: privateJson({ error: "request body is too large" }, { status: 413 }),
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
try {
|
|
20
|
-
const body = JSON.parse(raw);
|
|
21
|
-
return { ok: true, name: body.name };
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return {
|
|
25
|
-
ok: false,
|
|
26
|
-
response: privateJson({ error: "invalid JSON body" }, { status: 400 }),
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Interactive-operator lifecycle for deployment access tokens. The token itself
|
|
32
|
-
* is deliberately never an administrator credential and cannot reach here.
|
|
33
|
-
*/
|
|
34
|
-
export async function routeAccessTokens(context) {
|
|
35
|
-
const match = /^\/ui\/access-tokens(?:\/([0-9a-f-]{36}))?$/.exec(context.path);
|
|
36
|
-
if (!match)
|
|
37
|
-
return null;
|
|
38
|
-
const { request, baseUrl, opts } = context;
|
|
39
|
-
if (request.method === "OPTIONS") {
|
|
40
|
-
return privateJson({ error: "CORS is not allowed" }, { status: 403 });
|
|
41
|
-
}
|
|
42
|
-
if (!opts.accessTokens) {
|
|
43
|
-
return privateJson({ error: "access token management is not configured" }, { status: 404 });
|
|
44
|
-
}
|
|
45
|
-
const mutating = request.method !== "GET";
|
|
46
|
-
if (mutating && !isSameOrigin(request, baseUrl)) {
|
|
47
|
-
return privateJson({ error: "same-origin request required" }, { status: 403 });
|
|
48
|
-
}
|
|
49
|
-
const admin = await authorizeUiAdmin(request, baseUrl, opts.auth, "access token management", context.runtimeContext, opts.identity);
|
|
50
|
-
if (!admin.ok)
|
|
51
|
-
return admin.response;
|
|
52
|
-
const id = match[1];
|
|
53
|
-
try {
|
|
54
|
-
if (!id && request.method === "GET") {
|
|
55
|
-
return privateJson({ accessTokens: await opts.accessTokens.list() });
|
|
56
|
-
}
|
|
57
|
-
if (!id && request.method === "POST") {
|
|
58
|
-
const input = await readName(request);
|
|
59
|
-
if (!input.ok)
|
|
60
|
-
return input.response;
|
|
61
|
-
return privateJson(await opts.accessTokens.create(input.name, admin.principal ?? admin.userId), { status: 201 });
|
|
62
|
-
}
|
|
63
|
-
if (id && request.method === "PUT") {
|
|
64
|
-
const input = await readName(request);
|
|
65
|
-
if (!input.ok)
|
|
66
|
-
return input.response;
|
|
67
|
-
const accessToken = await opts.accessTokens.rename(id, input.name);
|
|
68
|
-
return accessToken
|
|
69
|
-
? privateJson({ accessToken })
|
|
70
|
-
: privateJson({ error: "unknown access token" }, { status: 404 });
|
|
71
|
-
}
|
|
72
|
-
if (id && request.method === "DELETE") {
|
|
73
|
-
const accessToken = await opts.accessTokens.revoke(id, admin.userId);
|
|
74
|
-
return accessToken
|
|
75
|
-
? privateJson({ accessToken })
|
|
76
|
-
: privateJson({ error: "unknown access token" }, { status: 404 });
|
|
77
|
-
}
|
|
78
|
-
return privateJson({ error: "method not allowed" }, { status: 405 });
|
|
79
|
-
}
|
|
80
|
-
catch (error) {
|
|
81
|
-
return privateJson({ error: msg(error) }, { status: 400 });
|
|
82
|
-
}
|
|
83
|
-
}
|