@devosurf/tesser-sdk 0.1.0-alpha.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/LICENSE +202 -0
- package/README.md +36 -0
- package/llms.txt +90 -0
- package/package.json +23 -0
- package/src/automation.ts +258 -0
- package/src/connector/index.ts +529 -0
- package/src/errors.ts +46 -0
- package/src/events.ts +25 -0
- package/src/harnesses.ts +76 -0
- package/src/index.ts +75 -0
- package/src/internal/client.ts +66 -0
- package/src/internal/codec.ts +135 -0
- package/src/internal/duration.ts +57 -0
- package/src/internal/harnesses.ts +46 -0
- package/src/internal/http.ts +179 -0
- package/src/internal/index.ts +51 -0
- package/src/internal/manifest.ts +398 -0
- package/src/internal/operators.ts +287 -0
- package/src/internal/retry.ts +57 -0
- package/src/internal/standard-schema.ts +92 -0
- package/src/internal/webhook-verify.ts +49 -0
- package/src/operators.ts +181 -0
- package/src/triggers.ts +79 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
// Connector authoring contract (ADR-0012) + connector-defined triggers (ADR-0013).
|
|
2
|
+
// Auth is declared once and injected as a pre-authed ctx.http — the author never names a
|
|
3
|
+
// token. Actions are typed calls returning OUR mapped stable shape. Triggers are typed
|
|
4
|
+
// constructors symmetric to actions; the runtime owns delivery/registration/dedup.
|
|
5
|
+
|
|
6
|
+
import type { Connector, Logger, Schema } from "../automation.js";
|
|
7
|
+
import type { HarnessDef, HarnessRunRequest, HarnessRunResult } from "../harnesses.js";
|
|
8
|
+
import type { NormalizedModelRequest, NormalizedModelResponse } from "../operators.js";
|
|
9
|
+
import type { ConnectorTrigger } from "../triggers.js";
|
|
10
|
+
import type { ClassifyError, TesserHttp } from "../internal/http.js";
|
|
11
|
+
import type { StandardSchemaV1 } from "../internal/standard-schema.js";
|
|
12
|
+
|
|
13
|
+
/** Any Standard Schema, keeping both its input (pre-parse: defaults optional) and
|
|
14
|
+
* output (post-parse) types. Callers supply the INPUT shape; handlers receive OUTPUT. */
|
|
15
|
+
export type AnySchema = StandardSchemaV1<any, any>;
|
|
16
|
+
export type SchemaIn<S> = S extends StandardSchemaV1<infer I, any> ? I : never;
|
|
17
|
+
export type SchemaOut<S> = S extends StandardSchemaV1<any, infer O> ? O : never;
|
|
18
|
+
|
|
19
|
+
// ---- Provider facts (the Catalog entry shape; inline facts are promoted by codegen, ADR-0012) ----
|
|
20
|
+
|
|
21
|
+
export interface OAuth2ProviderFacts {
|
|
22
|
+
authorizeUrl: string;
|
|
23
|
+
tokenUrl: string;
|
|
24
|
+
/** How the client authenticates at the token endpoint. Default "body". */
|
|
25
|
+
clientAuth?: "body" | "basic";
|
|
26
|
+
/** Scope list separator quirk. Default " ". */
|
|
27
|
+
scopeSeparator?: string;
|
|
28
|
+
/** Use PKCE on the auth-code flow. Default true (harmless where unsupported is false). */
|
|
29
|
+
pkce?: boolean;
|
|
30
|
+
/** Extra params some providers require (e.g. Google access_type=offline&prompt=consent). */
|
|
31
|
+
extraAuthorizeParams?: Record<string, string>;
|
|
32
|
+
extraTokenParams?: Record<string, string>;
|
|
33
|
+
/** Providers that rotate refresh tokens on every refresh. */
|
|
34
|
+
rotatesRefreshToken?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ProviderFacts {
|
|
38
|
+
id: string;
|
|
39
|
+
displayName?: string;
|
|
40
|
+
/** Default API base URL for connectors behind this provider. */
|
|
41
|
+
baseUrl?: string;
|
|
42
|
+
oauth2?: OAuth2ProviderFacts;
|
|
43
|
+
docsUrl?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---- Auth declarations (declared once; placement applied by the runtime, ADR-0012) ----
|
|
47
|
+
|
|
48
|
+
export interface OAuth2Auth {
|
|
49
|
+
readonly kind: "oauth2";
|
|
50
|
+
readonly provider?: string;
|
|
51
|
+
readonly scopes: readonly string[];
|
|
52
|
+
readonly flow: "auth_code" | "client_credentials";
|
|
53
|
+
readonly describe?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ApiKeyAuth {
|
|
57
|
+
readonly kind: "apiKey";
|
|
58
|
+
readonly in: "header" | "query";
|
|
59
|
+
readonly name: string;
|
|
60
|
+
readonly prefix?: string;
|
|
61
|
+
readonly describe?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface BasicAuth {
|
|
65
|
+
readonly kind: "basic";
|
|
66
|
+
readonly describe?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CustomAuth {
|
|
70
|
+
readonly kind: "custom";
|
|
71
|
+
readonly describe?: string;
|
|
72
|
+
/** Field names the connect page collects (empty = connection is instantly ready). */
|
|
73
|
+
readonly fields?: readonly string[];
|
|
74
|
+
/** Programmatic signer — receives the outbound request and the credential fields. */
|
|
75
|
+
readonly sign: (
|
|
76
|
+
req: { url: URL; headers: Headers },
|
|
77
|
+
fields: Readonly<Record<string, string>>,
|
|
78
|
+
) => void | Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type AuthDecl = OAuth2Auth | ApiKeyAuth | BasicAuth | CustomAuth;
|
|
82
|
+
|
|
83
|
+
export function oauth2(opts: {
|
|
84
|
+
provider?: string;
|
|
85
|
+
scopes: readonly string[];
|
|
86
|
+
flow?: "auth_code" | "client_credentials";
|
|
87
|
+
describe?: string;
|
|
88
|
+
}): OAuth2Auth {
|
|
89
|
+
return Object.freeze({
|
|
90
|
+
kind: "oauth2" as const,
|
|
91
|
+
scopes: Object.freeze([...opts.scopes]),
|
|
92
|
+
flow: opts.flow ?? "auth_code",
|
|
93
|
+
...(opts.provider !== undefined ? { provider: opts.provider } : {}),
|
|
94
|
+
...(opts.describe !== undefined ? { describe: opts.describe } : {}),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function apiKey(opts: {
|
|
99
|
+
in?: "header" | "query";
|
|
100
|
+
name?: string;
|
|
101
|
+
prefix?: string;
|
|
102
|
+
describe?: string;
|
|
103
|
+
}): ApiKeyAuth {
|
|
104
|
+
return Object.freeze({
|
|
105
|
+
kind: "apiKey" as const,
|
|
106
|
+
in: opts?.in ?? "header",
|
|
107
|
+
name: opts?.name ?? "Authorization",
|
|
108
|
+
...(opts?.prefix !== undefined ? { prefix: opts.prefix } : {}),
|
|
109
|
+
...(opts?.describe !== undefined ? { describe: opts.describe } : {}),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function basic(opts?: { describe?: string }): BasicAuth {
|
|
114
|
+
return Object.freeze({
|
|
115
|
+
kind: "basic" as const,
|
|
116
|
+
...(opts?.describe !== undefined ? { describe: opts.describe } : {}),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function customAuth(opts: {
|
|
121
|
+
describe?: string;
|
|
122
|
+
fields?: readonly string[];
|
|
123
|
+
sign: CustomAuth["sign"];
|
|
124
|
+
}): CustomAuth {
|
|
125
|
+
return Object.freeze({
|
|
126
|
+
kind: "custom" as const,
|
|
127
|
+
sign: opts.sign,
|
|
128
|
+
...(opts.describe !== undefined ? { describe: opts.describe } : {}),
|
|
129
|
+
...(opts.fields !== undefined ? { fields: Object.freeze([...opts.fields]) } : {}),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---- Actions (ADR-0012): non-durable, run inside the caller's Step ----
|
|
134
|
+
|
|
135
|
+
/** Runtime-resolved, log-masked credential reference — the escape hatch for non-standard
|
|
136
|
+
* placement. Prefer ctx.http, which attaches the credential by declared placement. */
|
|
137
|
+
export interface AuthRef {
|
|
138
|
+
readonly kind: AuthDecl["kind"];
|
|
139
|
+
/** Which mode of a named auth map this connection used (undefined for single-auth). */
|
|
140
|
+
readonly mode?: string;
|
|
141
|
+
/** Credential fields (e.g. access_token, api_key, username, password). Values are
|
|
142
|
+
* masked in logs by the runtime. */
|
|
143
|
+
readonly fields: Readonly<Record<string, string>>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface ActionCtx {
|
|
147
|
+
/** Pre-authed, base-URL'd client. Status → RetryableError/TerminalError per ADR-0012. */
|
|
148
|
+
readonly http: TesserHttp;
|
|
149
|
+
readonly auth: AuthRef;
|
|
150
|
+
/** Auto-derived (runId + step + occurrence); attach to provider idempotency headers. */
|
|
151
|
+
readonly idempotencyKey?: string;
|
|
152
|
+
readonly logger: Logger;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface ModelProviderSpec {
|
|
156
|
+
readonly aliases?: Record<string, string>;
|
|
157
|
+
readonly call: (ctx: ActionCtx, request: NormalizedModelRequest) => Promise<NormalizedModelResponse>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface HarnessProviderSpec {
|
|
161
|
+
readonly adapter: string;
|
|
162
|
+
readonly run: (
|
|
163
|
+
ctx: ActionCtx,
|
|
164
|
+
request: HarnessRunRequest<unknown>,
|
|
165
|
+
def: HarnessDef,
|
|
166
|
+
) => Promise<HarnessRunResult<unknown>>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** I = caller-facing input (schema input type), PI = parsed input handed to run,
|
|
170
|
+
* O = validated output. */
|
|
171
|
+
export interface ActionDef<I, PI, O> {
|
|
172
|
+
readonly __action: true;
|
|
173
|
+
readonly describe?: string;
|
|
174
|
+
readonly input: StandardSchemaV1<I, PI>;
|
|
175
|
+
readonly output: Schema<O>;
|
|
176
|
+
/** Reads retry freely; writes retry only with an idempotency key (derived, ADR-0012). */
|
|
177
|
+
readonly safety: "read" | "write";
|
|
178
|
+
/** Explicit override of derived retry-safety. */
|
|
179
|
+
readonly retrySafe?: boolean;
|
|
180
|
+
readonly classifyError?: ClassifyError;
|
|
181
|
+
readonly run: (ctx: ActionCtx, input: PI) => Promise<O>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export type AnyAction = ActionDef<any, any, any>;
|
|
185
|
+
export type ActionsTree = { [key: string]: AnyAction | ActionsTree };
|
|
186
|
+
|
|
187
|
+
export function action<IS extends AnySchema, OS extends AnySchema>(def: {
|
|
188
|
+
describe?: string;
|
|
189
|
+
input: IS;
|
|
190
|
+
output: OS;
|
|
191
|
+
safety?: "read" | "write";
|
|
192
|
+
retrySafe?: boolean;
|
|
193
|
+
classifyError?: ClassifyError;
|
|
194
|
+
/** Receives the PARSED input (defaults applied); returns the value `output` validates. */
|
|
195
|
+
run: (ctx: ActionCtx, input: SchemaOut<IS>) => Promise<SchemaIn<OS>>;
|
|
196
|
+
}): ActionDef<SchemaIn<IS>, SchemaOut<IS>, SchemaOut<OS>> {
|
|
197
|
+
if (!def?.input || !def?.output || typeof def?.run !== "function") {
|
|
198
|
+
throw new TypeError("action: requires { input, output, run }");
|
|
199
|
+
}
|
|
200
|
+
return Object.freeze({
|
|
201
|
+
__action: true as const,
|
|
202
|
+
input: def.input,
|
|
203
|
+
output: def.output,
|
|
204
|
+
safety: def.safety ?? "write",
|
|
205
|
+
run: def.run,
|
|
206
|
+
...(def.describe !== undefined ? { describe: def.describe } : {}),
|
|
207
|
+
...(def.retrySafe !== undefined ? { retrySafe: def.retrySafe } : {}),
|
|
208
|
+
...(def.classifyError !== undefined ? { classifyError: def.classifyError } : {}),
|
|
209
|
+
}) as never;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function isAction(node: unknown): node is AnyAction {
|
|
213
|
+
return typeof node === "object" && node !== null && (node as AnyAction).__action === true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---- Webhook receive (connector-level, ADR-0013) ----
|
|
217
|
+
|
|
218
|
+
export interface InboundWebhookEvent {
|
|
219
|
+
readonly headers: Record<string, string>;
|
|
220
|
+
readonly query: Record<string, string>;
|
|
221
|
+
readonly rawBody: Uint8Array;
|
|
222
|
+
/** Parsed JSON body, or undefined when the body is not JSON. */
|
|
223
|
+
readonly json: unknown;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export type VerifyScheme =
|
|
227
|
+
| {
|
|
228
|
+
kind: "hmacSha256";
|
|
229
|
+
header: string;
|
|
230
|
+
prefix?: string;
|
|
231
|
+
encoding: "hex" | "base64";
|
|
232
|
+
}
|
|
233
|
+
| { kind: "slackSigning" }
|
|
234
|
+
| {
|
|
235
|
+
kind: "custom";
|
|
236
|
+
verify: (req: InboundWebhookEvent, secret: string) => boolean | Promise<boolean>;
|
|
237
|
+
}
|
|
238
|
+
| { kind: "none" };
|
|
239
|
+
|
|
240
|
+
export const verify = {
|
|
241
|
+
hmacSha256(opts: { header: string; prefix?: string; encoding?: "hex" | "base64" }): VerifyScheme {
|
|
242
|
+
return Object.freeze({
|
|
243
|
+
kind: "hmacSha256" as const,
|
|
244
|
+
header: opts.header,
|
|
245
|
+
encoding: opts.encoding ?? "hex",
|
|
246
|
+
...(opts.prefix !== undefined ? { prefix: opts.prefix } : {}),
|
|
247
|
+
});
|
|
248
|
+
},
|
|
249
|
+
slackSigning(): VerifyScheme {
|
|
250
|
+
return Object.freeze({ kind: "slackSigning" as const });
|
|
251
|
+
},
|
|
252
|
+
custom(fn: (req: InboundWebhookEvent, secret: string) => boolean | Promise<boolean>): VerifyScheme {
|
|
253
|
+
return Object.freeze({ kind: "custom" as const, verify: fn });
|
|
254
|
+
},
|
|
255
|
+
none(): VerifyScheme {
|
|
256
|
+
return Object.freeze({ kind: "none" as const });
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
export interface IdentifiedEvent {
|
|
261
|
+
/** Provider event name a trigger's `event` matches against (e.g. "issues", "message"). */
|
|
262
|
+
event: string;
|
|
263
|
+
/** Provider delivery id used for dedup; fall back to a payload hash when absent. */
|
|
264
|
+
deliveryId?: string;
|
|
265
|
+
/** Provider-side account identity (team id, installation id) to resolve the Connection. */
|
|
266
|
+
connectionHint?: string;
|
|
267
|
+
/** The payload handed to the trigger's map(); defaults to the request JSON. */
|
|
268
|
+
payload?: unknown;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface WebhookReceive {
|
|
272
|
+
verify: VerifyScheme;
|
|
273
|
+
/** Pull event name / delivery id / connection identity out of any inbound request.
|
|
274
|
+
* Return null for requests that are not events (the runtime 400s them). */
|
|
275
|
+
identify: (req: InboundWebhookEvent) => IdentifiedEvent | null;
|
|
276
|
+
/** Endpoint-verification handshakes (e.g. Slack url_verification): return the response
|
|
277
|
+
* body to answer with, or null when the request is a normal event. */
|
|
278
|
+
challenge?: (req: InboundWebhookEvent) => string | null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---- Connector triggers (ADR-0013) ----
|
|
282
|
+
|
|
283
|
+
export interface RegistrationTarget {
|
|
284
|
+
/** Our ingress URL — stable per automation+trigger; never changes on redeploy. */
|
|
285
|
+
url: string;
|
|
286
|
+
/** The signing secret (we generate it in auto mode; the human pastes it in manual mode). */
|
|
287
|
+
secret: string;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface AutoRegistration<I> {
|
|
291
|
+
mode: "auto";
|
|
292
|
+
/** Create the provider-side hook using the brokered token (ctx.http). */
|
|
293
|
+
create: (
|
|
294
|
+
ctx: ActionCtx,
|
|
295
|
+
reg: RegistrationTarget,
|
|
296
|
+
params: I,
|
|
297
|
+
) => Promise<{ externalId?: string; state?: unknown } | void>;
|
|
298
|
+
/** Remove the provider-side hook on undeploy (no orphaned hooks). */
|
|
299
|
+
destroy?: (
|
|
300
|
+
ctx: ActionCtx,
|
|
301
|
+
reg: { externalId?: string; state?: unknown },
|
|
302
|
+
params: I,
|
|
303
|
+
) => Promise<void>;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export interface ManualRegistration<I> {
|
|
307
|
+
mode: "manual";
|
|
308
|
+
/** Connect-page copy: exact steps, with the ingress URL + secret available to interpolate. */
|
|
309
|
+
instructions: (reg: RegistrationTarget, params: I) => string;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export interface WebhookTriggerDecl<IS extends AnySchema = AnySchema, OS extends AnySchema = AnySchema> {
|
|
313
|
+
readonly __trigger: "webhook";
|
|
314
|
+
readonly describe?: string;
|
|
315
|
+
readonly input: IS;
|
|
316
|
+
readonly output: OS;
|
|
317
|
+
/** Which identified provider event this trigger consumes. */
|
|
318
|
+
readonly event: string;
|
|
319
|
+
/** Raw provider payload → our stable typed payload; return null to skip (fine filter). */
|
|
320
|
+
readonly map: (
|
|
321
|
+
payload: unknown,
|
|
322
|
+
params: SchemaOut<IS>,
|
|
323
|
+
) => SchemaIn<OS> | null | Promise<SchemaIn<OS> | null>;
|
|
324
|
+
readonly register: AutoRegistration<SchemaOut<IS>> | ManualRegistration<SchemaOut<IS>>;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export interface PollTriggerDecl<
|
|
328
|
+
IS extends AnySchema = AnySchema,
|
|
329
|
+
OS extends AnySchema = AnySchema,
|
|
330
|
+
Item = unknown,
|
|
331
|
+
> {
|
|
332
|
+
readonly __trigger: "poll";
|
|
333
|
+
readonly describe?: string;
|
|
334
|
+
readonly input: IS;
|
|
335
|
+
readonly output: OS;
|
|
336
|
+
/** Connector-declared cadence; automation-author override is clamped to `floor`. */
|
|
337
|
+
readonly interval?: { default: string; floor?: string };
|
|
338
|
+
/** Which way the provider's natural list reads. New items fire oldest-first either way.
|
|
339
|
+
* Default "newest-first" (how most list APIs sort). */
|
|
340
|
+
readonly order?: "newest-first" | "oldest-first";
|
|
341
|
+
/** Return the provider's natural current list — no cursor bookkeeping (runtime-owned
|
|
342
|
+
* windowed seen-set). Opt into cursor mode by returning { items, nextCursor }. */
|
|
343
|
+
readonly poll: (
|
|
344
|
+
ctx: ActionCtx,
|
|
345
|
+
params: SchemaOut<IS>,
|
|
346
|
+
cursor: unknown,
|
|
347
|
+
) => Promise<Item[] | { items: Item[]; nextCursor?: unknown }>;
|
|
348
|
+
readonly dedupeKey: (item: Item) => string;
|
|
349
|
+
/** Raw item → stable typed payload (default: identity). */
|
|
350
|
+
readonly map?: (item: Item, params: SchemaOut<IS>) => SchemaIn<OS> | Promise<SchemaIn<OS>>;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export type AnyTriggerDecl = WebhookTriggerDecl<AnySchema, AnySchema> | PollTriggerDecl<AnySchema, AnySchema, any>;
|
|
354
|
+
export type TriggersDecl = Record<string, AnyTriggerDecl>;
|
|
355
|
+
|
|
356
|
+
export const trigger = {
|
|
357
|
+
webhook<IS extends AnySchema, OS extends AnySchema>(def: {
|
|
358
|
+
describe?: string;
|
|
359
|
+
input: IS;
|
|
360
|
+
output: OS;
|
|
361
|
+
event: string;
|
|
362
|
+
map: (
|
|
363
|
+
payload: unknown,
|
|
364
|
+
params: SchemaOut<IS>,
|
|
365
|
+
) => SchemaIn<OS> | null | Promise<SchemaIn<OS> | null>;
|
|
366
|
+
register: AutoRegistration<SchemaOut<IS>> | ManualRegistration<SchemaOut<IS>>;
|
|
367
|
+
}): WebhookTriggerDecl<IS, OS> {
|
|
368
|
+
if (!def?.input || !def?.output || !def?.event || typeof def?.map !== "function" || !def?.register) {
|
|
369
|
+
throw new TypeError("trigger.webhook: requires { input, output, event, map, register }");
|
|
370
|
+
}
|
|
371
|
+
return Object.freeze({ __trigger: "webhook" as const, ...def });
|
|
372
|
+
},
|
|
373
|
+
poll<IS extends AnySchema, OS extends AnySchema, Item = SchemaIn<OS>>(def: {
|
|
374
|
+
describe?: string;
|
|
375
|
+
input: IS;
|
|
376
|
+
output: OS;
|
|
377
|
+
interval?: { default: string; floor?: string };
|
|
378
|
+
order?: "newest-first" | "oldest-first";
|
|
379
|
+
poll: (
|
|
380
|
+
ctx: ActionCtx,
|
|
381
|
+
params: SchemaOut<IS>,
|
|
382
|
+
cursor: unknown,
|
|
383
|
+
) => Promise<Item[] | { items: Item[]; nextCursor?: unknown }>;
|
|
384
|
+
dedupeKey: (item: Item) => string;
|
|
385
|
+
map?: (item: Item, params: SchemaOut<IS>) => SchemaIn<OS> | Promise<SchemaIn<OS>>;
|
|
386
|
+
}): PollTriggerDecl<IS, OS, Item> {
|
|
387
|
+
if (!def?.input || !def?.output || typeof def?.poll !== "function" || typeof def?.dedupeKey !== "function") {
|
|
388
|
+
throw new TypeError("trigger.poll: requires { input, output, poll, dedupeKey }");
|
|
389
|
+
}
|
|
390
|
+
return Object.freeze({ __trigger: "poll" as const, ...def });
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// ---- defineConnector ----
|
|
395
|
+
|
|
396
|
+
/** Optional-arg ergonomics: when every field is optional (or has a default), the call
|
|
397
|
+
* needs no argument at all. */
|
|
398
|
+
type ClientFn<I, O> = undefined extends I
|
|
399
|
+
? (input?: I) => Promise<O>
|
|
400
|
+
: Record<string, never> extends I
|
|
401
|
+
? (input?: I) => Promise<O>
|
|
402
|
+
: (input: I) => Promise<O>;
|
|
403
|
+
|
|
404
|
+
export type ClientFromActions<A> = {
|
|
405
|
+
readonly [K in keyof A]: A[K] extends ActionDef<infer I, any, infer O>
|
|
406
|
+
? ClientFn<I, O>
|
|
407
|
+
: A[K] extends ActionsTree
|
|
408
|
+
? ClientFromActions<A[K]>
|
|
409
|
+
: never;
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
export type TriggerConstructors<T extends TriggersDecl> = {
|
|
413
|
+
readonly [K in keyof T]: (
|
|
414
|
+
params: SchemaIn<T[K]["input"]> & { connection?: string } & (T[K] extends { __trigger: "poll" }
|
|
415
|
+
? { every?: string }
|
|
416
|
+
: Record<never, never>),
|
|
417
|
+
) => ConnectorTrigger<SchemaOut<T[K]["output"]>>;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
export interface ConnectorSpec<A extends ActionsTree, T extends TriggersDecl> {
|
|
421
|
+
id: string;
|
|
422
|
+
describe?: string;
|
|
423
|
+
/** Provider id referencing the Catalog, or inline facts (promoted by codegen, ADR-0012). */
|
|
424
|
+
provider?: string | ProviderFacts;
|
|
425
|
+
/** API base URL; defaults to the provider's. */
|
|
426
|
+
baseUrl?: string;
|
|
427
|
+
/** One declaration, or a named map for multiple modes (e.g. { oauth, token }). */
|
|
428
|
+
auth: AuthDecl | Record<string, AuthDecl>;
|
|
429
|
+
/** Headers every ctx.http request carries (e.g. Accept/API-version/User-Agent). */
|
|
430
|
+
defaultHeaders?: Record<string, string>;
|
|
431
|
+
/** Provider idempotency header — when declared, the runtime auto-attaches ctx.idempotencyKey. */
|
|
432
|
+
idempotencyHeader?: string;
|
|
433
|
+
actions: A;
|
|
434
|
+
triggers?: T;
|
|
435
|
+
/** Direct model-call capability (ADR-0016). Runtime-owned; not exposed as ctx.connections actions. */
|
|
436
|
+
modelProvider?: ModelProviderSpec;
|
|
437
|
+
/** Sandboxed external runner capability (ADR-0019). Runtime-owned; not exposed as an Action. */
|
|
438
|
+
harnessProvider?: HarnessProviderSpec;
|
|
439
|
+
/** Required when any trigger is webhook-strategy. */
|
|
440
|
+
webhook?: WebhookReceive;
|
|
441
|
+
/** Sample outputs by dotted action path (and `trigger:<id>` for trigger payloads) —
|
|
442
|
+
* powers auto-mocked tests (ADR-0008) and connect-page previews. */
|
|
443
|
+
samples?: Record<string, unknown>;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** The full connector object: the automation-facing Connector<client> plus trigger
|
|
447
|
+
* constructors plus the raw spec for build-time extraction. */
|
|
448
|
+
export interface ConnectorInstance<A extends ActionsTree, T extends TriggersDecl>
|
|
449
|
+
extends Connector<ClientFromActions<A>> {
|
|
450
|
+
readonly triggers: TriggerConstructors<T>;
|
|
451
|
+
/** Build-time/manifest access to the declaration — not for automation code. */
|
|
452
|
+
readonly __connector: ConnectorSpec<A, T>;
|
|
453
|
+
/** Narrowed override so `.perUser()` keeps the trigger constructors. */
|
|
454
|
+
perUser(): ConnectorInstance<A, T>;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const CONNECTOR_ID = /^[a-z][a-z0-9-]{0,63}$/;
|
|
458
|
+
const AUTH_KINDS = new Set(["oauth2", "apiKey", "basic", "custom"]);
|
|
459
|
+
|
|
460
|
+
function isAuthDecl(v: unknown): v is AuthDecl {
|
|
461
|
+
return typeof v === "object" && v !== null && AUTH_KINDS.has((v as AuthDecl).kind);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function defineConnector<
|
|
465
|
+
A extends ActionsTree,
|
|
466
|
+
T extends TriggersDecl = Record<string, never>,
|
|
467
|
+
>(spec: ConnectorSpec<A, T>): ConnectorInstance<A, T> {
|
|
468
|
+
if (!CONNECTOR_ID.test(spec?.id ?? "")) {
|
|
469
|
+
throw new TypeError(`defineConnector: id "${String(spec?.id)}" must be kebab-case`);
|
|
470
|
+
}
|
|
471
|
+
if (!spec.auth || (!isAuthDecl(spec.auth) && Object.values(spec.auth).every((a) => !isAuthDecl(a)))) {
|
|
472
|
+
throw new TypeError(`defineConnector(${spec.id}): auth must be oauth2/apiKey/basic/customAuth (or a named map)`);
|
|
473
|
+
}
|
|
474
|
+
if ((!spec.actions || Object.keys(spec.actions).length === 0) && !spec.modelProvider && !spec.harnessProvider) {
|
|
475
|
+
throw new TypeError(`defineConnector(${spec.id}): at least one action, modelProvider, or harnessProvider is required`);
|
|
476
|
+
}
|
|
477
|
+
const normalized = { ...spec, actions: (spec.actions ?? ({} as A)) } as ConnectorSpec<A, T>;
|
|
478
|
+
const triggers = normalized.triggers ?? ({} as T);
|
|
479
|
+
const hasWebhookTrigger = Object.values(triggers).some((t) => t.__trigger === "webhook");
|
|
480
|
+
if (hasWebhookTrigger && !normalized.webhook) {
|
|
481
|
+
throw new TypeError(
|
|
482
|
+
`defineConnector(${spec.id}): webhook-strategy triggers require the connector-level webhook { verify, identify }`,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const constructors: Record<string, (params: Record<string, unknown>) => ConnectorTrigger<unknown>> = {};
|
|
487
|
+
for (const [triggerId, decl] of Object.entries(triggers)) {
|
|
488
|
+
void decl;
|
|
489
|
+
constructors[triggerId] = (params) => {
|
|
490
|
+
// `connection` and `every` are reserved platform params, not provider filters.
|
|
491
|
+
const { connection, every, ...rest } = params ?? {};
|
|
492
|
+
return Object.freeze({
|
|
493
|
+
kind: "connector" as const,
|
|
494
|
+
connectorId: normalized.id,
|
|
495
|
+
triggerId,
|
|
496
|
+
params: rest,
|
|
497
|
+
...(connection !== undefined ? { connectionKey: String(connection) } : {}),
|
|
498
|
+
...(every !== undefined ? { every: String(every) } : {}),
|
|
499
|
+
});
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function make(scope: "workspace" | "per_user"): ConnectorInstance<A, T> {
|
|
504
|
+
const instance = {
|
|
505
|
+
id: normalized.id,
|
|
506
|
+
scope,
|
|
507
|
+
triggers: Object.freeze({ ...constructors }) as TriggerConstructors<T>,
|
|
508
|
+
__connector: normalized,
|
|
509
|
+
perUser(): ConnectorInstance<A, T> {
|
|
510
|
+
return make("per_user");
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
return Object.freeze(instance) as unknown as ConnectorInstance<A, T>;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return make("workspace");
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function isConnector(value: unknown): value is ConnectorInstance<ActionsTree, TriggersDecl> {
|
|
520
|
+
return (
|
|
521
|
+
typeof value === "object" &&
|
|
522
|
+
value !== null &&
|
|
523
|
+
typeof (value as { id?: unknown }).id === "string" &&
|
|
524
|
+
typeof (value as { __connector?: unknown }).__connector === "object"
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export type { ClassifyError, TesserHttp } from "../internal/http.js";
|
|
529
|
+
export type { Schema } from "../automation.js";
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Errors control the retry policy (ADR-0002). Symbol branding keeps instanceof checks
|
|
2
|
+
// honest across duplicated module instances (bundled server vs project artifact).
|
|
3
|
+
|
|
4
|
+
const RETRYABLE = Symbol.for("tesser.error.retryable");
|
|
5
|
+
const TERMINAL = Symbol.for("tesser.error.terminal");
|
|
6
|
+
|
|
7
|
+
export interface RetryableErrorOptions {
|
|
8
|
+
cause?: unknown;
|
|
9
|
+
/** Hint from the provider (e.g. Retry-After) — the engine waits at least this long. */
|
|
10
|
+
retryAfterMs?: number | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Force a retry (also the default for an uncaught throw, up to maxAttempts). */
|
|
14
|
+
export class RetryableError extends Error {
|
|
15
|
+
readonly retryAfterMs: number | undefined;
|
|
16
|
+
|
|
17
|
+
constructor(message: string, options?: RetryableErrorOptions) {
|
|
18
|
+
super(message, options?.cause === undefined ? undefined : { cause: options.cause });
|
|
19
|
+
this.name = "RetryableError";
|
|
20
|
+
this.retryAfterMs = options?.retryAfterMs;
|
|
21
|
+
Object.defineProperty(this, RETRYABLE, { value: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Stop now — do not retry. On terminal failure, completed steps' `undo` run in reverse. */
|
|
26
|
+
export class TerminalError extends Error {
|
|
27
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
28
|
+
super(message, options?.cause === undefined ? undefined : { cause: options.cause });
|
|
29
|
+
this.name = "TerminalError";
|
|
30
|
+
Object.defineProperty(this, TERMINAL, { value: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isRetryableError(err: unknown): err is RetryableError {
|
|
35
|
+
return (
|
|
36
|
+
err instanceof RetryableError ||
|
|
37
|
+
(typeof err === "object" && err !== null && RETRYABLE in err)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isTerminalError(err: unknown): err is TerminalError {
|
|
42
|
+
return (
|
|
43
|
+
err instanceof TerminalError ||
|
|
44
|
+
(typeof err === "object" && err !== null && TERMINAL in err)
|
|
45
|
+
);
|
|
46
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Events: defined once, reused for emit + onEvent (+ future await). Project-scoped (ADR-0011).
|
|
2
|
+
|
|
3
|
+
import type { StandardSchemaV1 } from "./internal/standard-schema.js";
|
|
4
|
+
|
|
5
|
+
export interface EventDefinition<T> {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly schema: StandardSchemaV1<unknown, T>;
|
|
8
|
+
/** Phantom for inference; never set. */
|
|
9
|
+
readonly _payload?: T;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const EVENT_NAME = /^[a-z][a-z0-9]*([.-][a-z0-9]+)*$/;
|
|
13
|
+
|
|
14
|
+
/** Define an event once; reuse it for emit + trigger (no stringly-typed drift). */
|
|
15
|
+
export function defineEvent<T>(
|
|
16
|
+
name: string,
|
|
17
|
+
schema: StandardSchemaV1<unknown, T>,
|
|
18
|
+
): EventDefinition<T> {
|
|
19
|
+
if (!EVENT_NAME.test(name)) {
|
|
20
|
+
throw new TypeError(
|
|
21
|
+
`defineEvent: "${name}" — event names are lower-case dot/dash-separated, e.g. "invoice.paid"`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return Object.freeze({ name, schema });
|
|
25
|
+
}
|
package/src/harnesses.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Schema, Serializable } from "./automation.js";
|
|
2
|
+
|
|
3
|
+
export interface HarnessDef {
|
|
4
|
+
readonly __harness: true;
|
|
5
|
+
readonly connection: string;
|
|
6
|
+
readonly sandbox: "none" | "repo-readonly" | "repo-write";
|
|
7
|
+
readonly permissions: "read-only" | "workspace-write";
|
|
8
|
+
readonly timeout?: string;
|
|
9
|
+
readonly maxOutputBytes?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface HarnessBudget {
|
|
13
|
+
invocations: number;
|
|
14
|
+
wallClock?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type HarnessMap = Record<string, HarnessDef>;
|
|
18
|
+
|
|
19
|
+
export interface HarnessRunRequest<TOutput = unknown> {
|
|
20
|
+
prompt: string;
|
|
21
|
+
input?: Serializable;
|
|
22
|
+
output: Schema<TOutput>;
|
|
23
|
+
timeout?: string;
|
|
24
|
+
maxOutputBytes?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface HarnessArtifact extends Record<string, Serializable> {
|
|
28
|
+
name: string;
|
|
29
|
+
kind: "transcript" | "log" | "patch" | "file" | "screenshot" | "json";
|
|
30
|
+
bytes?: number;
|
|
31
|
+
path?: string;
|
|
32
|
+
content?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface HarnessRunResult<TOutput = unknown> extends Record<string, Serializable> {
|
|
36
|
+
output: TOutput & Serializable;
|
|
37
|
+
status: "completed" | "failed";
|
|
38
|
+
exitCode?: number;
|
|
39
|
+
artifacts: HarnessArtifact[];
|
|
40
|
+
transcript?: string;
|
|
41
|
+
adapter: string;
|
|
42
|
+
raw?: Serializable;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type Harnesses<H extends HarnessMap = HarnessMap> = {
|
|
46
|
+
readonly [K in keyof H]: {
|
|
47
|
+
run<TOutput>(request: HarnessRunRequest<TOutput>): Promise<HarnessRunResult<TOutput>>;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function harness(def: {
|
|
52
|
+
connection: string;
|
|
53
|
+
sandbox?: "none" | "repo-readonly" | "repo-write";
|
|
54
|
+
permissions?: "read-only" | "workspace-write";
|
|
55
|
+
timeout?: string;
|
|
56
|
+
maxOutputBytes?: number;
|
|
57
|
+
}): HarnessDef {
|
|
58
|
+
if (!def || typeof def.connection !== "string" || def.connection.length === 0) {
|
|
59
|
+
throw new TypeError("harness: connection must name a declared Harness connection");
|
|
60
|
+
}
|
|
61
|
+
if (def.maxOutputBytes !== undefined && (!Number.isInteger(def.maxOutputBytes) || def.maxOutputBytes < 1)) {
|
|
62
|
+
throw new TypeError("harness: maxOutputBytes must be a positive integer");
|
|
63
|
+
}
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
__harness: true as const,
|
|
66
|
+
connection: def.connection,
|
|
67
|
+
sandbox: def.sandbox ?? "repo-readonly",
|
|
68
|
+
permissions: def.permissions ?? "read-only",
|
|
69
|
+
...(def.timeout !== undefined ? { timeout: def.timeout } : {}),
|
|
70
|
+
...(def.maxOutputBytes !== undefined ? { maxOutputBytes: def.maxOutputBytes } : {}),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isHarnessDef(value: unknown): value is HarnessDef {
|
|
75
|
+
return typeof value === "object" && value !== null && (value as HarnessDef).__harness === true;
|
|
76
|
+
}
|