@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.
Files changed (78) hide show
  1. package/AGENTS.md +5 -0
  2. package/CHANGELOG.md +40 -0
  3. package/README.md +18 -10
  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/connectors/remote-mcp.js +3 -5
  13. package/dist/credential-contract.d.ts +24 -0
  14. package/dist/credential-contract.js +1 -0
  15. package/dist/credential-rules.d.ts +85 -0
  16. package/dist/credential-rules.js +107 -0
  17. package/dist/credentials.d.ts +4 -100
  18. package/dist/credentials.js +3 -107
  19. package/dist/index.d.ts +22 -55
  20. package/dist/index.js +30 -58
  21. package/dist/invocation.js +2 -3
  22. package/dist/meta-tools.d.ts +4 -0
  23. package/dist/meta-tools.js +8 -4
  24. package/dist/module-contracts.d.ts +19 -0
  25. package/dist/module-contracts.js +1 -0
  26. package/dist/operator-ui/generated.js +2 -2
  27. package/dist/operator-ui/model.d.ts +6 -3
  28. package/dist/operator-ui/view.d.ts +2 -18
  29. package/dist/operator-ui/view.js +3 -20
  30. package/dist/registry.d.ts +4 -1
  31. package/dist/registry.js +4 -6
  32. package/dist/routes/activity.js +1 -1
  33. package/dist/routes/credentials.js +5 -2
  34. package/dist/routes/mcp.js +7 -3
  35. package/dist/routes/oauth-management.d.ts +2 -0
  36. package/dist/routes/oauth-management.js +108 -0
  37. package/dist/routes/oauth.d.ts +0 -1
  38. package/dist/routes/oauth.js +21 -121
  39. package/dist/routes/shared.d.ts +19 -17
  40. package/dist/routes/shared.js +48 -44
  41. package/dist/routes/ui.js +36 -33
  42. package/dist/server.js +6 -26
  43. package/dist/types.d.ts +2 -0
  44. package/dist/ui.d.ts +15 -70
  45. package/dist/ui.js +176 -317
  46. package/dist/version.d.ts +1 -1
  47. package/dist/version.js +1 -1
  48. package/documentation/architecture.md +26 -17
  49. package/documentation/auth.md +61 -106
  50. package/documentation/cloudflare.md +1 -1
  51. package/documentation/connectors.md +1 -1
  52. package/documentation/linear.md +1 -1
  53. package/documentation/meta-tools.md +6 -4
  54. package/documentation/mixpanel.md +1 -1
  55. package/documentation/notion.md +2 -2
  56. package/documentation/operations.md +12 -14
  57. package/documentation/operator-ui.md +82 -104
  58. package/documentation/optional-modules-upgrade.md +243 -0
  59. package/documentation/provider-conventions.md +5 -3
  60. package/documentation/revenuecat.md +1 -1
  61. package/documentation/storage-and-credentials.md +59 -40
  62. package/documentation/stripe.md +1 -1
  63. package/documentation/upgrading.md +29 -4
  64. package/ethos.md +22 -30
  65. package/examples/worker/AGENTS.md +3 -1
  66. package/examples/worker/README.md +68 -84
  67. package/examples/worker/src/d1-activity.ts +1 -1
  68. package/examples/worker/src/index.ts +11 -6
  69. package/package.json +18 -2
  70. package/templates/node/AGENTS.md +8 -6
  71. package/templates/node/README.md +56 -67
  72. package/templates/node/package.json +1 -1
  73. package/templates/node/src/file-activity.ts +1 -1
  74. package/templates/node/src/index.ts +11 -12
  75. package/dist/access-tokens.d.ts +0 -31
  76. package/dist/access-tokens.js +0 -236
  77. package/dist/routes/access-tokens.d.ts +0 -6
  78. package/dist/routes/access-tokens.js +0 -83
@@ -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
- }