@airfone/connector 0.0.0-stage → 0.1.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/dist/host.js ADDED
@@ -0,0 +1,219 @@
1
+ // Host calls for local runs. LocalHost is `dev`: real network (declared hosts
2
+ // only), a store kept in .airfone/store.json, records printed. ReviewHost is
3
+ // `test` and matches the server's review sandbox: no network, an in-memory
4
+ // store, records accepted and dropped.
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
+ import { dirname } from 'node:path';
7
+ import { applyAuth } from './http.js';
8
+ import { createHash, createHmac } from 'node:crypto';
9
+ import { allowedHosts, secretNames } from './manifest.js';
10
+ import { fetchSync } from './runner.js';
11
+ const MAX_KV_VALUE = 64 * 1024;
12
+ function pairs(v) {
13
+ if (!v || typeof v !== 'object')
14
+ return [];
15
+ return Object.entries(v).map(([k, x]) => [k, typeof x === 'string' ? x : JSON.stringify(x)]);
16
+ }
17
+ class BaseHost {
18
+ m;
19
+ config;
20
+ granted;
21
+ events = [];
22
+ constructor(m, config, granted) {
23
+ this.m = m;
24
+ this.config = config;
25
+ this.granted = granted;
26
+ }
27
+ call(op, args, deadline) {
28
+ const str = (k) => { const v = args?.[k]; if (typeof v !== 'string')
29
+ throw `${op}: ${k} is required`; return v; };
30
+ switch (op) {
31
+ case 'http.fetch': return this.fetch(args, deadline);
32
+ case 'store.get': return this.storeGet(str('key')) ?? null;
33
+ case 'store.set':
34
+ case 'store.delete': {
35
+ const key = str('key');
36
+ if (key.length === 0 || key.length > 200)
37
+ throw 'store keys are 1 to 200 characters';
38
+ if (op === 'store.delete') {
39
+ this.storeDelete(key);
40
+ return null;
41
+ }
42
+ const value = args.value ?? null;
43
+ if (JSON.stringify(value).length > MAX_KV_VALUE)
44
+ throw 'store values are at most 64 KB';
45
+ if (args && 'if_equals' in args) {
46
+ if (JSON.stringify(this.storeGet(key) ?? null) !== JSON.stringify(args.if_equals ?? null))
47
+ return false;
48
+ this.storeSet(key, value);
49
+ return true;
50
+ }
51
+ this.storeSet(key, value);
52
+ return null;
53
+ }
54
+ case 'store.incr': {
55
+ const key = str('key');
56
+ if (key.length === 0 || key.length > 200)
57
+ throw 'store keys are 1 to 200 characters';
58
+ const by = typeof args?.by === 'number' ? args.by : 1;
59
+ const cur = this.storeGet(key);
60
+ const n = (typeof cur === 'number' ? cur : 0) + by;
61
+ this.storeSet(key, n);
62
+ return n;
63
+ }
64
+ case 'crypto.hmac':
65
+ case 'crypto.hash': return cryptoOp(op, args, (n) => this.secret(n));
66
+ case 'records.upsert': {
67
+ const ok = (s) => this.m.permissions.scopes.includes(s) && this.granted.includes(s);
68
+ if (!ok('records.write'))
69
+ throw 'not allowed: this connection was not granted records.write';
70
+ const r = args?.records;
71
+ return this.upsert(Array.isArray(r) ? r : [r]);
72
+ }
73
+ case 'knowledge.upsert':
74
+ case 'knowledge.delete': {
75
+ const ok = (s) => this.m.permissions.scopes.includes(s) && this.granted.includes(s);
76
+ if (!ok('knowledge.write'))
77
+ throw 'not allowed: this connection was not granted knowledge.write';
78
+ if (op === 'knowledge.delete')
79
+ return this.knowledge('delete', [String(args?.external_id ?? '')]);
80
+ const r = args?.records;
81
+ const records = Array.isArray(r) ? r : [r];
82
+ if (records.length > 500)
83
+ throw 'at most 500 records per call';
84
+ return this.knowledge('upsert', records);
85
+ }
86
+ case 'log': {
87
+ const level = typeof args?.level === 'string' ? args.level : 'info';
88
+ this.log(level, String(args?.message ?? '').slice(0, 2000));
89
+ return null;
90
+ }
91
+ default: throw `unknown host call ${op}`;
92
+ }
93
+ }
94
+ }
95
+ const ALGS = { sha256: 'sha256', sha512: 'sha512' };
96
+ /** crypto.rs fill_secrets: each `{{secrets.x}}` becomes that declared secret. */
97
+ export function fillSecrets(text, secret) {
98
+ let out = '';
99
+ let rest = text;
100
+ for (let start = rest.indexOf('{{secrets.'); start >= 0; start = rest.indexOf('{{secrets.')) {
101
+ const after = rest.slice(start + 10);
102
+ const end = after.indexOf('}}');
103
+ if (end < 0)
104
+ throw 'unclosed {{secrets.';
105
+ const name = after.slice(0, end).trim();
106
+ const value = secret(name);
107
+ if (value === undefined)
108
+ throw `the install is missing secret ${JSON.stringify(name)}; reconnect it`;
109
+ out += rest.slice(0, start) + value;
110
+ rest = after.slice(end + 2);
111
+ }
112
+ return out + rest;
113
+ }
114
+ /** crypto.rs host_op: `{{secrets.x}}` in the message is filled in, keys never leave the host. */
115
+ export function cryptoOp(op, args, secret) {
116
+ const get = (k) => typeof args?.[k] === 'string' ? args[k] : undefined;
117
+ const algName = (get('alg') ?? 'sha256').toLowerCase().replaceAll('-', '');
118
+ const raw = get('message');
119
+ if (raw === undefined)
120
+ throw `${op}: message is required`;
121
+ // Filled only for an HMAC: a plain hash would let code guess the secret.
122
+ const message = op === 'crypto.hmac' ? fillSecrets(raw, secret) : raw;
123
+ const alg = ALGS[algName];
124
+ let bytes;
125
+ if (op === 'crypto.hmac') {
126
+ const name = get('secret');
127
+ if (name === undefined)
128
+ throw "crypto.hmac: secret (the secret's name) is required";
129
+ const key = secret(name);
130
+ if (!key)
131
+ throw `the install is missing secret ${JSON.stringify(name)}; reconnect it`;
132
+ if (!alg)
133
+ throw `unknown HMAC algorithm ${JSON.stringify(algName)}; use sha256 or sha512`;
134
+ bytes = createHmac(alg, key).update(message).digest();
135
+ }
136
+ else {
137
+ if (!alg)
138
+ throw `unknown hash ${JSON.stringify(algName)}; use sha256 or sha512`;
139
+ bytes = createHash(alg).update(message).digest();
140
+ }
141
+ const encoding = get('encoding') ?? 'hex';
142
+ if (encoding !== 'hex' && encoding !== 'base64')
143
+ throw `${op}: encoding ${JSON.stringify(encoding)} is hex or base64`;
144
+ return bytes.toString(encoding);
145
+ }
146
+ export class LocalHost extends BaseHost {
147
+ secrets;
148
+ storePath;
149
+ egress;
150
+ out;
151
+ store;
152
+ constructor(m, config, secrets, storePath, egress, out) {
153
+ super(m, config, m.permissions.scopes);
154
+ this.secrets = secrets;
155
+ this.storePath = storePath;
156
+ this.egress = egress;
157
+ this.out = out;
158
+ this.store = existsSync(storePath) ? JSON.parse(readFileSync(storePath, 'utf8')) : {};
159
+ }
160
+ save() { mkdirSync(dirname(this.storePath), { recursive: true }); writeFileSync(this.storePath, JSON.stringify(this.store, null, 2)); }
161
+ storeGet(key) { return this.store[key]; }
162
+ storeSet(key, value) { this.store[key] = value; this.save(); }
163
+ storeDelete(key) { delete this.store[key]; this.save(); }
164
+ fetch(args, deadline) {
165
+ const method = String(args?.method ?? 'GET').toUpperCase();
166
+ // The host owns auth: JS cannot set or override it.
167
+ const headers = pairs(args?.headers).filter(([k]) => k.toLowerCase() !== 'authorization');
168
+ const query = pairs(args?.query);
169
+ try {
170
+ applyAuth(this.m.auth, this.secrets, headers, query);
171
+ }
172
+ catch (e) {
173
+ throw e.message;
174
+ }
175
+ const req = { method, url: String(args?.url), headers, query, body: typeof args?.body === 'string'
176
+ // Declared secrets named as {{secrets.x}} are filled in by the host, as on the server.
177
+ ? (args.body.includes('{{secrets.') ? fillSecrets(args.body, (n) => this.secret(n)) : args.body)
178
+ : null };
179
+ const t = Date.now();
180
+ const resp = fetchSync(allowedHosts(this.m, this.config), req, this.egress, deadline);
181
+ this.out(`http ${method} ${req.url} -> ${resp.status} (${Date.now() - t} ms)`);
182
+ return { status: resp.status, headers: resp.headers, body: resp.body };
183
+ }
184
+ upsert(records) {
185
+ this.out(`records.upsert ${JSON.stringify(records)}`);
186
+ return { local: true, count: records.length };
187
+ }
188
+ knowledge(kind, items) {
189
+ this.out(`knowledge.${kind} ${JSON.stringify(items)}`);
190
+ return kind === 'delete' ? { local: true, deleted: true } : { local: true, results: items.map(() => ({ result: 'written' })) };
191
+ }
192
+ secret(name) {
193
+ if (!secretNames(this.m).includes(name))
194
+ return undefined;
195
+ const v = this.secrets[name];
196
+ return typeof v === 'string' ? v : undefined;
197
+ }
198
+ log(level, message) {
199
+ this.out(this.m.sensitive ? `log ${level} (sensitive: withheld on AirFone) ${message}` : `log ${level} ${message}`);
200
+ }
201
+ }
202
+ export class ReviewHost extends BaseHost {
203
+ store = new Map();
204
+ constructor(m, config) { super(m, config, m.permissions.scopes); }
205
+ storeGet(key) { return this.store.get(key); }
206
+ storeSet(key, value) { this.store.set(key, value); }
207
+ storeDelete(key) { this.store.delete(key); }
208
+ fetch() { throw 'no network during review; give fixtures that need none'; }
209
+ upsert() { return { review: true }; }
210
+ knowledge() { return { review: true }; }
211
+ log() { }
212
+ secret() { return 'review'; }
213
+ call(op, args, deadline) {
214
+ // The server's review host does not check scopes for records.
215
+ if (op === 'records.upsert' || op === 'knowledge.upsert' || op === 'knowledge.delete')
216
+ return { review: true };
217
+ return super.call(op, args, deadline);
218
+ }
219
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { type Canonical } from './manifest.js';
2
+ import { type EgressOptions, type OutRequest } from './net.js';
3
+ type Secrets = Record<string, string>;
4
+ /** `secrets` is readable as `{{secrets.x}}` (headers and query only); `keys` feeds the hmac filters anywhere. */
5
+ interface Scope {
6
+ input: unknown;
7
+ config: unknown;
8
+ secrets: Secrets | null;
9
+ keys?: Secrets | null;
10
+ }
11
+ export declare const encodeComponent: (s: string) => string;
12
+ /** `a ?? b | filter | filter` (template.rs Scope::evaluate). Undefined when every reference is absent. */
13
+ export declare function evaluate(expr: string, scope: Scope): unknown;
14
+ export declare const renderStr: (s: string, scope: Scope) => string;
15
+ export declare const renderUrl: (s: string, scope: Scope) => string;
16
+ export declare function renderValue(v: unknown, scope: Scope): unknown;
17
+ /** Adds the manifest's auth to a request (http_exec.rs apply_auth). */
18
+ export declare function applyAuth(auth: Canonical['auth'], secrets: Secrets, headers: [string, string][], query: [string, string][]): void;
19
+ export declare function buildRequest(m: Canonical, spec: any, input: unknown, config: unknown, secrets: Secrets): OutRequest;
20
+ export declare function mapOutput(map: Record<string, string> | undefined, body: unknown): unknown;
21
+ /** The answer as JSON (http_exec.rs response_json). An event stream gives its last JSON data event. */
22
+ export declare function responseJson(resp: {
23
+ headers: Record<string, string> | [string, string][];
24
+ body: string;
25
+ }): unknown;
26
+ /** http_exec.rs check_fail_rules. */
27
+ export declare function checkFailRules(rules: {
28
+ path: string;
29
+ equals?: unknown;
30
+ message?: string;
31
+ message_path?: string;
32
+ }[] | undefined, body: unknown): void;
33
+ export declare function runHttp(m: Canonical, action: any, input: unknown, config: unknown, secrets: Secrets, egress: EgressOptions): Promise<unknown>;
34
+ export {};
package/dist/http.js ADDED
@@ -0,0 +1,268 @@
1
+ // Declarative HTTP actions, as the server runs them (template.rs, http_exec.rs).
2
+ import { createHash, createHmac } from 'node:crypto';
3
+ import { JSONPath } from 'jsonpath-plus';
4
+ import { allowedHosts, withConfigDefaults } from './manifest.js';
5
+ import { send } from './net.js';
6
+ const refs = (s) => [...s.matchAll(/\{\{(.*?)\}\}/g)].map((m) => m[1].trim());
7
+ function lookup(root, path) {
8
+ let cur = root;
9
+ for (const p of path) {
10
+ if (Array.isArray(cur)) {
11
+ if (!/^\d+$/.test(p))
12
+ return undefined;
13
+ cur = cur[Number(p)];
14
+ }
15
+ else if (cur && typeof cur === 'object')
16
+ cur = cur[p];
17
+ else
18
+ return undefined;
19
+ if (cur === undefined)
20
+ return undefined;
21
+ }
22
+ return cur;
23
+ }
24
+ function resolve(ref, scope) {
25
+ const [head, ...rest] = ref.split('.');
26
+ if (head === 'secrets') {
27
+ if (!scope.secrets)
28
+ throw new Error('secrets can only be used in headers and query parameters');
29
+ if (rest.length !== 1)
30
+ throw new Error(`bad secret reference ${JSON.stringify(ref)}`);
31
+ return scope.secrets[rest[0]];
32
+ }
33
+ if (head === 'input')
34
+ return lookup(scope.input, rest);
35
+ if (head === 'config')
36
+ return lookup(scope.config, rest);
37
+ throw new Error(`unknown template root in {{${ref}}}`);
38
+ }
39
+ const asText = (v) => v === undefined || v === null ? '' : typeof v === 'string' ? v : JSON.stringify(v);
40
+ export const encodeComponent = (s) => [...new TextEncoder().encode(s)].map((b) => /[A-Za-z0-9\-_.~]/.test(String.fromCharCode(b)) ? String.fromCharCode(b) : `%${b.toString(16).toUpperCase().padStart(2, '0')}`).join('');
41
+ /** `a ?? b | filter | filter` (template.rs Scope::evaluate). Undefined when every reference is absent. */
42
+ export function evaluate(expr, scope) {
43
+ const [refsPart, ...filters] = expr.split('|');
44
+ let value;
45
+ for (const r of refsPart.split('??').map((x) => x.trim())) {
46
+ const v = resolve(r, scope);
47
+ if (v === undefined || v === null || v === '')
48
+ continue;
49
+ value = v;
50
+ break;
51
+ }
52
+ for (const f of filters.map((x) => x.trim())) {
53
+ if (value === undefined)
54
+ break;
55
+ value = filter(f, asText(value), scope);
56
+ }
57
+ return value;
58
+ }
59
+ function filter(f, text, scope) {
60
+ const open = f.indexOf('(');
61
+ const name = open < 0 ? f : f.slice(0, open).trim();
62
+ const arg = open < 0 ? null : f.slice(open + 1).replace(/\)+$/, '').trim().replace(/^["']+|["']+$/g, '');
63
+ const key = () => {
64
+ if (arg === null)
65
+ throw new Error(`${name} needs a secret name`);
66
+ const k = scope.keys?.[arg];
67
+ if (typeof k !== 'string')
68
+ throw new Error(`the install is missing secret ${JSON.stringify(arg)}; reconnect it`);
69
+ return k;
70
+ };
71
+ switch (name) {
72
+ case 'base64': return Buffer.from(text).toString('base64');
73
+ case 'urlencode': return encodeComponent(text);
74
+ case 'sha256': return createHash('sha256').update(text).digest('hex');
75
+ case 'hmac_sha256': return createHmac('sha256', key()).update(text).digest('hex');
76
+ case 'hmac_sha256_b64': return createHmac('sha256', key()).update(text).digest('base64');
77
+ default: throw new Error(`unknown template filter ${JSON.stringify(name)}`);
78
+ }
79
+ }
80
+ function renderWith(s, scope, encode) {
81
+ let out = '';
82
+ let rest = s;
83
+ for (;;) {
84
+ const start = rest.indexOf('{{');
85
+ if (start < 0)
86
+ break;
87
+ out += rest.slice(0, start);
88
+ const after = rest.slice(start + 2);
89
+ const end = after.indexOf('}}');
90
+ if (end < 0)
91
+ throw new Error(`unclosed {{ in template ${JSON.stringify(s)}`);
92
+ out += encode(out.length, asText(evaluate(after.slice(0, end).trim(), scope)));
93
+ rest = after.slice(end + 2);
94
+ }
95
+ return out + rest;
96
+ }
97
+ export const renderStr = (s, scope) => renderWith(s, scope, (_, t) => t);
98
+ export const renderUrl = (s, scope) => renderWith(s, scope, (at, t) => at === 0 ? t.replace(/\/+$/, '') : encodeComponent(t));
99
+ export function renderValue(v, scope) {
100
+ if (typeof v === 'string') {
101
+ const t = v.trim();
102
+ const r = refs(t);
103
+ if (isSingleReference(v) && r.length === 1)
104
+ return evaluate(r[0], scope) ?? null;
105
+ return renderStr(v, scope);
106
+ }
107
+ if (Array.isArray(v))
108
+ return v.map((x) => renderValue(x, scope));
109
+ if (v && typeof v === 'object') {
110
+ const out = {};
111
+ for (const [k, x] of Object.entries(v)) {
112
+ const r = renderValue(x, scope);
113
+ // A field that is only one reference, and that reference is absent, is left out.
114
+ if (r === null && isSingleReference(x))
115
+ continue;
116
+ out[k] = r;
117
+ }
118
+ return out;
119
+ }
120
+ return v;
121
+ }
122
+ function isSingleReference(v) {
123
+ if (typeof v !== 'string')
124
+ return false;
125
+ const t = v.trim();
126
+ return t.startsWith('{{') && t.endsWith('}}') && t.split('{{').length === 2;
127
+ }
128
+ function secret(secrets, name) {
129
+ const v = secrets[name];
130
+ if (typeof v !== 'string' || !v)
131
+ throw new Error(`the install is missing secret ${JSON.stringify(name)}; reconnect it`);
132
+ return v;
133
+ }
134
+ /** Adds the manifest's auth to a request (http_exec.rs apply_auth). */
135
+ export function applyAuth(auth, secrets, headers, query) {
136
+ const a = auth;
137
+ switch (a.kind) {
138
+ case 'api_key': {
139
+ const value = `${a.prefix ?? ''}${secret(secrets, a.secret)}`;
140
+ (a.in === 'query' ? query : headers).push([a.name, value]);
141
+ break;
142
+ }
143
+ case 'bearer':
144
+ headers.push(['Authorization', `Bearer ${secret(secrets, a.secret)}`]);
145
+ break;
146
+ case 'oauth':
147
+ headers.push(['Authorization', `${typeof a.prefix === 'string' ? a.prefix : 'Bearer '}${secret(secrets, a.secret)}`]);
148
+ break;
149
+ case 'basic':
150
+ headers.push(['Authorization', `Basic ${Buffer.from(`${secret(secrets, a.username)}:${secret(secrets, a.password)}`).toString('base64')}`]);
151
+ break;
152
+ }
153
+ }
154
+ export function buildRequest(m, spec, input, config, secrets) {
155
+ const plain = { input, config, secrets: null, keys: secrets };
156
+ const withSecrets = { input, config, secrets, keys: secrets };
157
+ const url = renderUrl(spec.url, plain);
158
+ const headers = Object.entries(spec.headers ?? {}).map(([k, v]) => [k, renderStr(String(v), withSecrets)]);
159
+ const query = [];
160
+ for (const [k, v] of Object.entries(spec.query ?? {})) {
161
+ const value = renderStr(String(v), withSecrets);
162
+ if (value)
163
+ query.push([k, value]);
164
+ }
165
+ applyAuth(m.auth, secrets, headers, query);
166
+ let body = null;
167
+ if (spec.body !== undefined && spec.body !== null) {
168
+ if (!headers.some(([k]) => k.toLowerCase() === 'content-type'))
169
+ headers.push(['Content-Type', 'application/json']);
170
+ body = JSON.stringify(renderValue(spec.body, plain));
171
+ }
172
+ return { method: String(spec.method).toUpperCase(), url, headers, query, body };
173
+ }
174
+ export function mapOutput(map, body) {
175
+ if (!map || Object.keys(map).length === 0)
176
+ return body;
177
+ const out = {};
178
+ for (const [field, path] of Object.entries(map)) {
179
+ let nodes;
180
+ try {
181
+ nodes = JSONPath({ path, json: body, wrap: true });
182
+ }
183
+ catch (e) {
184
+ throw new Error(`output ${field}: bad JSONPath ${JSON.stringify(path)}: ${e.message}`);
185
+ }
186
+ const many = path.includes('[*]') || path.includes('..') || path.includes('[?');
187
+ out[field] = many ? nodes : nodes[0] ?? null;
188
+ }
189
+ return out;
190
+ }
191
+ const short = (s) => s.slice(0, 200).replaceAll('\n', ' ');
192
+ /** The answer as JSON (http_exec.rs response_json). An event stream gives its last JSON data event. */
193
+ export function responseJson(resp) {
194
+ if (!resp.body.trim())
195
+ return null;
196
+ const headers = Array.isArray(resp.headers) ? resp.headers : Object.entries(resp.headers);
197
+ const start = resp.body.trimStart();
198
+ const sse = headers.some(([k, v]) => k.toLowerCase() === 'content-type' && String(v).startsWith('text/event-stream')) || start.startsWith('data:') || start.startsWith('event:');
199
+ if (sse) {
200
+ const last = lastSseJson(resp.body);
201
+ if (last === undefined)
202
+ throw new Error(`the event stream has no JSON event: ${short(resp.body)}`);
203
+ return last;
204
+ }
205
+ try {
206
+ return JSON.parse(resp.body);
207
+ }
208
+ catch (e) {
209
+ throw new Error(`the answer is not JSON (${e.message}): ${short(resp.body)}`);
210
+ }
211
+ }
212
+ function lastSseJson(body) {
213
+ let data = '';
214
+ let last;
215
+ for (const line of [...body.split(/\r?\n/), '']) {
216
+ if (line.startsWith('data:'))
217
+ data += line.slice(5).trimStart();
218
+ else if (line === '' && data) {
219
+ try {
220
+ last = JSON.parse(data);
221
+ }
222
+ catch { /* not JSON */ }
223
+ data = '';
224
+ }
225
+ }
226
+ return last;
227
+ }
228
+ /** http_exec.rs check_fail_rules. */
229
+ export function checkFailRules(rules, body) {
230
+ const first = (path) => {
231
+ try {
232
+ const r = JSONPath({ path, json: body, wrap: true });
233
+ return r.length ? r[0] : undefined;
234
+ }
235
+ catch (e) {
236
+ throw new Error(`fail_when: ${e.message}`);
237
+ }
238
+ };
239
+ for (const rule of rules ?? []) {
240
+ const hit = first(rule.path);
241
+ const failed = rule.equals !== undefined && rule.equals !== null ? hit !== undefined && JSON.stringify(hit) === JSON.stringify(rule.equals) : hit !== undefined && hit !== null;
242
+ if (!failed)
243
+ continue;
244
+ let said;
245
+ if (rule.message_path) {
246
+ try {
247
+ const v = first(rule.message_path);
248
+ if (v !== undefined)
249
+ said = typeof v === 'string' ? v : JSON.stringify(v);
250
+ }
251
+ catch { /* ignored */ }
252
+ }
253
+ throw new Error(rule.message ?? said ?? 'the service refused the request');
254
+ }
255
+ }
256
+ export async function runHttp(m, action, input, config, secrets, egress) {
257
+ config = withConfigDefaults(m.config, config);
258
+ const req = buildRequest(m, action.http, input, config, secrets);
259
+ const resp = await send(allowedHosts(m, config), req, egress);
260
+ const msg = action.http.errors?.[String(resp.status)];
261
+ if (msg)
262
+ throw new Error(msg);
263
+ if (resp.status < 200 || resp.status >= 300)
264
+ throw new Error(`HTTP ${resp.status}: ${short(resp.body)}`);
265
+ const body = responseJson(resp);
266
+ checkFailRules(action.http.fail_when, body);
267
+ return mapOutput(action.http.output, body);
268
+ }
@@ -0,0 +1,8 @@
1
+ export type * from './types.js';
2
+ import type { Actions } from './types.js';
3
+ /** Types your actions object. Returns it unchanged: `export const actions = defineActions({ ... })`. */
4
+ export declare function defineActions<A extends Actions>(actions: A): A;
5
+ export { parseManifest, normalize, checkSchema, permissionDiff, ManifestError, SCOPES } from './manifest.js';
6
+ export { runAction, compileCheck, DEFAULT_LIMITS, ScriptError, type HostBridge, type Limits } from './runner.js';
7
+ export { pack, sha256Hex, signingMessage, signBundle } from './bundle.js';
8
+ export { review, type Finding } from './review.js';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /** Types your actions object. Returns it unchanged: `export const actions = defineActions({ ... })`. */
2
+ export function defineActions(actions) {
3
+ return actions;
4
+ }
5
+ export { parseManifest, normalize, checkSchema, permissionDiff, ManifestError, SCOPES } from './manifest.js';
6
+ export { runAction, compileCheck, DEFAULT_LIMITS, ScriptError } from './runner.js';
7
+ export { pack, sha256Hex, signingMessage, signBundle } from './bundle.js';
8
+ export { review } from './review.js';
@@ -0,0 +1,6 @@
1
+ export declare const KNOWLEDGE_HELP = "airfone knowledge <command>\n\nCommands\n push <source> <file> Upsert the records in a JSON array or JSON Lines file\n into <source> (a-z, 0-9, - and _). 500 per request.\n delete <source> <id> Delete one record by its external_id.\n sources List your sources with record counts.\n\nEach record: {\"external_id\", \"kind\", \"name\", \"aliases\"?, \"facts\": [{\"relation\",\n\"text\", \"audience\"?, \"volatile\"?, \"valid_from\"?, \"valid_to\"?}], \"links\"?, \"updated_at\"?}\n\nNeeds AIRFONE_KEY (a secret API key with knowledge:write) or AIRFONE_TOKEN, and\nAIRFONE_API (default https://app.airfone.app).";
2
+ type Out = (s: string) => void;
3
+ /** Records from a JSON array file or a JSON Lines file. */
4
+ export declare function readRecords(path: string): unknown[];
5
+ export declare function knowledge(pos: string[], out: Out, err: Out): Promise<boolean>;
6
+ export {};