@passioncode-ai/fabric-agent-adapter 0.39.1 → 0.40.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/CHANGELOG.md +79 -0
- package/README.md +9 -3
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +15 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +64 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +15 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/knowledge.example.json +13 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +264 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +93 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +128 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/usage.md +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +10 -183
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +204 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +286 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +327 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +47 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +79 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +840 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +340 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +42 -1237
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +1 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs +41 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +13 -45
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +387 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +8 -67
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +52 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +231 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs +67 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +202 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-logs.mjs +136 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +335 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +2 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +69 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +164 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +1 -32
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +122 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +177 -2541
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +1 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +400 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-time.mjs +46 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +458 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +402 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +399 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +78 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +64 -1398
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +116 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +902 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +457 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +36 -38
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +289 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +15 -180
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +586 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +39 -400
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +137 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +402 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +1 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +310 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_kit.py +243 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +308 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_logs.py +202 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +431 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +21 -43
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +102 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +338 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +31 -38
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +173 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +298 -3450
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +8 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +412 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_time.py +120 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +697 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +1526 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +3 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +116 -1392
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +270 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +508 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_hosts.py +265 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +155 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +335 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +1 -0
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// Node's own crypto: ES256 through createPublicKey (JWK) and verify (DER), EdDSA through Ed25519.
|
|
12
12
|
|
|
13
13
|
import crypto from 'node:crypto';
|
|
14
|
-
import { jcs } from './fabric-
|
|
14
|
+
import { jcs } from './fabric-jsvalue.mjs';
|
|
15
15
|
import { normalIdentifier } from './fabric-identifiers.mjs';
|
|
16
16
|
import { secretValue } from './fabric-secrets.mjs';
|
|
17
17
|
|
|
@@ -23,28 +23,7 @@ export const CLOCK_SKEW_SECONDS = 60;
|
|
|
23
23
|
export const ASSERTION_ALGORITHMS = Object.freeze(['ES256', 'EdDSA']);
|
|
24
24
|
export const FLAGS = Object.freeze({ UP: 0x01, UV: 0x04, BE: 0x08, BS: 0x10, AT: 0x40, ED: 0x80 });
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
const isString = (value) => typeof value === 'string';
|
|
28
|
-
const list = (value) => (Array.isArray(value) ? value.filter(isObject) : []);
|
|
29
|
-
/** The object items of an array with their index as written, so an instancePath names the item even after a non-object. */
|
|
30
|
-
const indexed = (value) => (Array.isArray(value) ? value.flatMap((item, index) => (isObject(item) ? [[item, index]] : [])) : []);
|
|
31
|
-
/** The contract's `show`: String(value), or its JSON where String() throws (an object whose own `toString` is data). */
|
|
32
|
-
const show = (value) => {
|
|
33
|
-
if (typeof value === 'string') return value;
|
|
34
|
-
try {
|
|
35
|
-
return String(value);
|
|
36
|
-
} catch {
|
|
37
|
-
return JSON.stringify(value) ?? typeof value;
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
/** The contract's `toNumber`: Number(value), or NaN where Number() throws. */
|
|
41
|
-
const toNumber = (value) => {
|
|
42
|
-
try {
|
|
43
|
-
return Number(value);
|
|
44
|
-
} catch {
|
|
45
|
-
return Number.NaN;
|
|
46
|
-
}
|
|
47
|
-
};
|
|
26
|
+
import { isObject, isString, objects as list, indexed, show, toNumber, keyOf } from './fabric-jsvalue.mjs'; // the core's (FAA-65)
|
|
48
27
|
/** DEC-0082: the personal context, and an organization's context `org:<orgId>` (the contract's `context-route.ts`). */
|
|
49
28
|
export const PERSONAL_CONTEXT = 'personal';
|
|
50
29
|
export const ORG_CONTEXT_PREFIX = 'org:';
|
|
@@ -59,43 +38,15 @@ export function parseContext(value) {
|
|
|
59
38
|
}
|
|
60
39
|
/** A placement's or a secret placement's context: its `context`, `personal` when absent (DEC-0082). */
|
|
61
40
|
const contextOf = (entry) => (entry.context === undefined ? PERSONAL_CONTEXT : entry.context);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest();
|
|
41
|
+
import { sha256, isSmallOrderOrMalformed as smallOrder, isValidP256Key } from './fabric-crypto.mjs'; // the core's (FAA-65)
|
|
42
|
+
export { isValidP256Key };
|
|
65
43
|
const B64URL = /^[A-Za-z0-9_-]+$/;
|
|
66
44
|
const bytes = (text) => Buffer.from(text, 'base64url');
|
|
67
45
|
const canonical = (text) => Buffer.from(text, 'base64url').toString('base64url') === text;
|
|
68
46
|
|
|
69
|
-
const SMALL_ORDER = new Set([
|
|
70
|
-
'0000000000000000000000000000000000000000000000000000000000000000', '0100000000000000000000000000000000000000000000000000000000000000',
|
|
71
|
-
'26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05', 'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a',
|
|
72
|
-
'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f', 'edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
|
|
73
|
-
'eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
|
|
74
|
-
]);
|
|
75
|
-
/** The contract's isSmallOrderKey: a key that is not 32 bytes counts as one too. */
|
|
76
|
-
function smallOrder(raw) {
|
|
77
|
-
if (raw.length !== 32) return true;
|
|
78
|
-
const cleared = Buffer.from(raw);
|
|
79
|
-
cleared[31] &= 0x7f;
|
|
80
|
-
return SMALL_ORDER.has(cleared.toString('hex'));
|
|
81
|
-
}
|
|
82
47
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const P = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffffn;
|
|
86
|
-
const B = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604bn;
|
|
87
|
-
const toBig = (part) => BigInt(`0x${Buffer.from(part).toString('hex')}`);
|
|
88
|
-
|
|
89
|
-
/** Whether `raw` is a valid uncompressed P-256 public key: 0x04 ‖ X ‖ Y, both below p, on the curve. */
|
|
90
|
-
export function isValidP256Key(raw) {
|
|
91
|
-
if (raw.length !== 65 || raw[0] !== 0x04) return false;
|
|
92
|
-
const x = toBig(raw.subarray(1, 33));
|
|
93
|
-
const y = toBig(raw.subarray(33, 65));
|
|
94
|
-
if (x >= P || y >= P) return false;
|
|
95
|
-
const left = (y * y) % P;
|
|
96
|
-
const right = (((x * x) % P) * x - 3n * x + B) % P;
|
|
97
|
-
return left === ((right % P) + P) % P;
|
|
98
|
-
}
|
|
48
|
+
|
|
49
|
+
|
|
99
50
|
|
|
100
51
|
// ---- WebAuthn Level 3 §7.2, offline ----
|
|
101
52
|
|
|
@@ -180,17 +131,8 @@ export const estateChallengeInput = (digest) => Buffer.from(jcs({ type: ESTATE_C
|
|
|
180
131
|
/** The WebAuthn challenge a signer passes to navigator.credentials.get(): SHA-256 of estateChallengeInput. */
|
|
181
132
|
export const estateChallenge = (digest) => sha256(estateChallengeInput(digest));
|
|
182
133
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
export function estateTime(value) {
|
|
186
|
-
const match = isString(value) ? RFC3339.exec(value) : null;
|
|
187
|
-
if (!match) return Number.NaN;
|
|
188
|
-
const [year, month, day, hour, minute, second] = match.slice(1, 7).map(Number);
|
|
189
|
-
const date = new Date(0);
|
|
190
|
-
date.setUTCFullYear(year, month - 1, day);
|
|
191
|
-
if (date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day || hour > 23 || minute > 59 || second > 59) return Number.NaN;
|
|
192
|
-
return Date.parse(value);
|
|
193
|
-
}
|
|
134
|
+
import { parseRfc3339Ms as estateTime, clockMs as clockOf } from './fabric-time.mjs'; // the core's (FAA-65)
|
|
135
|
+
export { estateTime };
|
|
194
136
|
const LABEL = '[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?';
|
|
195
137
|
/** `sign.` and at least two more labels, lower case: a signing RP ID is never a bare `sign.<tld>` (DEC-0063). */
|
|
196
138
|
const SIGNING_RP = new RegExp(`^sign(?:\\.${LABEL}){2,}$`);
|
|
@@ -220,7 +162,6 @@ function signingCredential(credential, path, flag) {
|
|
|
220
162
|
}
|
|
221
163
|
|
|
222
164
|
const revocationKey = (entry) => `${String(entry.kind)} ${String(entry.ref)}`;
|
|
223
|
-
const clockOf = (at) => (at instanceof Date ? at.getTime() : (typeof at === 'number' && Number.isFinite(at) ? at * 1000 : Number.NaN));
|
|
224
165
|
|
|
225
166
|
/**
|
|
226
167
|
* The reference verifier, in the order of estate-record.md's ER table. `at` is the clock (a Date, or epoch seconds); pass
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// A service's events (DEC-0029): making them, paging them and the JSONL event log.
|
|
2
|
+
// Part of the Node service kit (FAA-65, layer L3); the twin of fabric_events.py. Node 20+, no dependency.
|
|
3
|
+
import { ServiceError } from './fabric-base.mjs';
|
|
4
|
+
import { feedDataProblems } from './fabric-protocols.mjs';
|
|
5
|
+
|
|
6
|
+
export const LEVELS = ['info', 'notice', 'warning', 'error'];
|
|
7
|
+
|
|
8
|
+
export const EVENTS_DEFAULT_LIMIT = 50;
|
|
9
|
+
|
|
10
|
+
export const EVENTS_MAX_LIMIT = 200;
|
|
11
|
+
|
|
12
|
+
const KIND = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*){0,5}$/;
|
|
13
|
+
|
|
14
|
+
const TRACE_ID = /^(?!0{32}$)[0-9a-f]{32}$/;
|
|
15
|
+
|
|
16
|
+
const SPAN_ID = /^(?!0{16}$)[0-9a-f]{16}$/;
|
|
17
|
+
|
|
18
|
+
/** One activity event. `data` (DEC-0070) adds machine-readable fields, `{schema, value}`, never a secret: refused here, so no feed carries one. */
|
|
19
|
+
export function makeEvent(id, at, kind, level, text, { subject, link, notify, traceId, spanId, data } = {}) {
|
|
20
|
+
if (!LEVELS.includes(level)) throw new ServiceError(`level must be one of ${LEVELS.join(', ')}.`);
|
|
21
|
+
if (!KIND.test(kind)) throw new ServiceError(`kind ${kind} must be dotted lowercase.`);
|
|
22
|
+
const sentence = String(text).split(/\s+/).filter(Boolean).join(' ');
|
|
23
|
+
if (!sentence) throw new ServiceError('An event needs a sentence.');
|
|
24
|
+
if (link !== undefined && (!link.startsWith('/') || link.startsWith('//'))) throw new ServiceError('link must be a path on this service.');
|
|
25
|
+
if ((traceId === undefined) !== (spanId === undefined)) throw new ServiceError('An event carries traceId and spanId together, or neither.');
|
|
26
|
+
if (traceId !== undefined && !(TRACE_ID.test(traceId) && SPAN_ID.test(spanId))) throw new ServiceError('traceId is 32 and spanId 16 lowercase hex characters, not all zeros.');
|
|
27
|
+
const event = { id: String(id), at, kind, level, text: sentence.slice(0, 500) };
|
|
28
|
+
if (subject) event.subject = subject;
|
|
29
|
+
if (link) event.link = link;
|
|
30
|
+
if (notify) event.notify = true;
|
|
31
|
+
if (traceId !== undefined) Object.assign(event, { traceId, spanId });
|
|
32
|
+
if (data !== undefined) {
|
|
33
|
+
const problems = feedDataProblems(data);
|
|
34
|
+
if (problems.length) throw new ServiceError(`Event data: ${problems.join('; ')}.`);
|
|
35
|
+
event.data = { schema: data.schema, value: data.value };
|
|
36
|
+
}
|
|
37
|
+
return event;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseLimit(raw) {
|
|
41
|
+
if (raw === undefined || raw === null || raw === '') return EVENTS_DEFAULT_LIMIT;
|
|
42
|
+
const n = Number(raw);
|
|
43
|
+
if (!Number.isInteger(n)) throw new ServiceError('limit must be an integer.');
|
|
44
|
+
return Math.max(1, Math.min(EVENTS_MAX_LIMIT, n));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// fetch(after, limit) returns events with ids greater than `after`, ascending
|
|
48
|
+
// (or the newest `limit`, ascending, when after is null) — a view over the log you already keep.
|
|
49
|
+
export async function eventsPage(fetch, after, limit) {
|
|
50
|
+
const events = (await fetch(after ?? null, limit)).slice(0, limit);
|
|
51
|
+
return { events, cursor: events.length ? events.at(-1).id : (after ?? null) };
|
|
52
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// An outbound HTTP client for a service: one deadline across every attempt, bounded retries with jittered backoff,
|
|
2
|
+
// `Retry-After` honoured up to a cap, a response size cap, and no secret in an error. The twin of fabric_http.py:
|
|
3
|
+
// the same rules, the same cases (test/fixtures/kit-http-cases.json).
|
|
4
|
+
//
|
|
5
|
+
// const response = await request('GET', 'https://api.example/v1/items', { headers: {...}, deadlineS: 20, retries: 2 });
|
|
6
|
+
// response.status, response.headers['content-type'], response.json()
|
|
7
|
+
//
|
|
8
|
+
// What is retried: a status in RETRY_STATUSES or a connection error, at most `retries` times, and only when repeating
|
|
9
|
+
// is safe: an idempotent method or an `Idempotency-Key` header; any other request only on 429. The wait before retry n
|
|
10
|
+
// (from 0) is min(maxBackoffS, backoffBaseS * 2**n) * (0.5 + rand()); a `Retry-After` replaces it up to
|
|
11
|
+
// maxRetryAfterS. A wait past the deadline is not taken. Plain http is refused unless the host is loopback or
|
|
12
|
+
// `insecure`. An error names the method and the redacted URL, never a header or a body.
|
|
13
|
+
//
|
|
14
|
+
// The transport is injected in tests: async ({ method, url, headers, body, timeoutS, maxBytes }) =>
|
|
15
|
+
// { status, headers, body: Uint8Array }, throwing on a connection error (a `TimeoutError` name is a timeout) or an
|
|
16
|
+
// HttpError. Node 20 or newer, no dependencies. Part of the service kit (FAA-65, layer L4).
|
|
17
|
+
|
|
18
|
+
export const RETRY_STATUSES = Object.freeze([408, 425, 429, 500, 502, 503, 504]);
|
|
19
|
+
export const IDEMPOTENT_METHODS = Object.freeze(['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE']);
|
|
20
|
+
export const DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
|
|
21
|
+
const TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
22
|
+
|
|
23
|
+
export class HttpError extends Error {
|
|
24
|
+
constructor(message, { status = null, url = '', attempts = 0, retryAfterS = null, body = new Uint8Array(0),
|
|
25
|
+
kind = 'http' } = {}) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = 'HttpError';
|
|
28
|
+
this.status = status;
|
|
29
|
+
this.url = url;
|
|
30
|
+
this.attempts = attempts;
|
|
31
|
+
this.retryAfterS = retryAfterS;
|
|
32
|
+
this.body = body.slice(0, 2048);
|
|
33
|
+
this.kind = kind; // http, connection, timeout, too-large, refused
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class Response {
|
|
38
|
+
constructor(status, headers, body, { attempts = 1, url = '' } = {}) {
|
|
39
|
+
this.status = status;
|
|
40
|
+
this.headers = Object.fromEntries(Object.entries(headers).map(([k, v]) => [String(k).toLowerCase(), String(v)]));
|
|
41
|
+
this.body = body;
|
|
42
|
+
this.attempts = attempts;
|
|
43
|
+
this.url = url;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get ok() { return this.status >= 200 && this.status < 300; }
|
|
47
|
+
|
|
48
|
+
text() { return new TextDecoder().decode(this.body); }
|
|
49
|
+
|
|
50
|
+
json() { return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(this.body)); }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The URL as it may appear in an error or a log line: scheme, host, port and path; no userinfo, query or fragment.
|
|
54
|
+
export function redactUrl(url) {
|
|
55
|
+
let parts;
|
|
56
|
+
try { parts = new URL(String(url)); } catch { return '<unparseable url>'; }
|
|
57
|
+
return `${parts.protocol}//${parts.host}${parts.pathname}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isLoopback(host) {
|
|
61
|
+
const h = host.replace(/^\[|\]$/g, '');
|
|
62
|
+
if (h === 'localhost' || h.endsWith('.localhost') || h === '::1') return true;
|
|
63
|
+
return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Refuse what this client never sends: another scheme, no host, credentials in the URL, plain http off loopback.
|
|
67
|
+
export function checkUrl(url, { insecure = false } = {}) {
|
|
68
|
+
let parts;
|
|
69
|
+
try { parts = new URL(String(url)); } catch {
|
|
70
|
+
throw new HttpError('the URL does not parse', { url: redactUrl(url), kind: 'refused' });
|
|
71
|
+
}
|
|
72
|
+
if (!['http:', 'https:'].includes(parts.protocol) || !parts.hostname) {
|
|
73
|
+
throw new HttpError('only http and https URLs with a host are sent', { url: redactUrl(url), kind: 'refused' });
|
|
74
|
+
}
|
|
75
|
+
if (parts.username || parts.password || /^[a-z]+:\/*[^/?#]*@/i.test(String(url).trim())) {
|
|
76
|
+
throw new HttpError('credentials in a URL are refused: send them in a header', { url: redactUrl(url), kind: 'refused' });
|
|
77
|
+
}
|
|
78
|
+
if (parts.protocol === 'http:' && !insecure && !isLoopback(parts.hostname)) {
|
|
79
|
+
throw new HttpError('plain http to a host that is not loopback is refused (insecure to allow)',
|
|
80
|
+
{ url: redactUrl(url), kind: 'refused' });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const IMF_FIXDATE = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d\d) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d\d):(\d\d):(\d\d) GMT$/;
|
|
85
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
86
|
+
|
|
87
|
+
// Seconds a `Retry-After` asks for: a non-negative integer, or an IMF-fixdate read from `now` (epoch seconds),
|
|
88
|
+
// never below 0; null otherwise.
|
|
89
|
+
export function retryAfterS(value, { now } = {}) {
|
|
90
|
+
if (value === null || value === undefined) return null;
|
|
91
|
+
const text = String(value).trim();
|
|
92
|
+
if (/^[0-9]{1,10}$/.test(text)) return Number(text);
|
|
93
|
+
const m = IMF_FIXDATE.exec(text);
|
|
94
|
+
if (!m) return null;
|
|
95
|
+
const [day, month, year, hh, mm, ss] = [Number(m[2]), MONTHS.indexOf(m[3]), Number(m[4]), Number(m[5]),
|
|
96
|
+
Number(m[6]), Number(m[7])];
|
|
97
|
+
const lastDay = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
|
|
98
|
+
if (hh > 23 || mm > 59 || ss > 60 || day < 1 || day > lastDay || year < 1000) return null; // Date.UTC maps 0-99
|
|
99
|
+
const when = Date.UTC(year, month, day, hh, mm, Math.min(ss, 59)) / 1000;
|
|
100
|
+
return Math.max(0, when - (now ?? Date.now() / 1000));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Compact JSON in UTF-8, NaN and Infinity refused (JSON.stringify would write null): the twin of json_bytes.
|
|
104
|
+
export function jsonBytes(value) {
|
|
105
|
+
const text = JSON.stringify(value, (key, v) => {
|
|
106
|
+
if (typeof v === 'number' && !Number.isFinite(v)) {
|
|
107
|
+
throw new HttpError('the body holds a number JSON cannot write (NaN or Infinity)', { kind: 'refused' });
|
|
108
|
+
}
|
|
109
|
+
return v;
|
|
110
|
+
});
|
|
111
|
+
return new TextEncoder().encode(text);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function backoffS(attempt, { baseS, maxS, rand }) {
|
|
115
|
+
return Math.min(maxS, baseS * 2 ** attempt) * (0.5 + rand);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function mayRetry(method, headers, status) {
|
|
119
|
+
if (status !== null && !RETRY_STATUSES.includes(status)) return false;
|
|
120
|
+
const safe = IDEMPOTENT_METHODS.includes(String(method).toUpperCase())
|
|
121
|
+
|| Object.keys(headers).some((k) => k.toLowerCase() === 'idempotency-key');
|
|
122
|
+
return safe || status === 429;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function checkHeaders(headers) {
|
|
126
|
+
const out = {};
|
|
127
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
128
|
+
const v = String(value);
|
|
129
|
+
if (!TOKEN.test(String(name))) throw new HttpError('a header name is not an HTTP token', { kind: 'refused' });
|
|
130
|
+
if (/[\r\n\0]/.test(v)) throw new HttpError('a header value carries a line break or NUL', { kind: 'refused' });
|
|
131
|
+
out[String(name)] = v;
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The default transport: fetch, no redirects followed, the body read to one byte past maxBytes.
|
|
137
|
+
export async function fetchTransport({ method, url, headers, body, timeoutS, maxBytes }) {
|
|
138
|
+
const res = await fetch(url, { method, headers, body: body ?? undefined, redirect: 'manual',
|
|
139
|
+
signal: AbortSignal.timeout(Math.max(1, Math.ceil(timeoutS * 1000))) });
|
|
140
|
+
const chunks = [];
|
|
141
|
+
let size = 0;
|
|
142
|
+
if (res.body) {
|
|
143
|
+
const reader = res.body.getReader();
|
|
144
|
+
try {
|
|
145
|
+
while (size <= maxBytes) {
|
|
146
|
+
const { done, value } = await reader.read();
|
|
147
|
+
if (done) break;
|
|
148
|
+
chunks.push(value);
|
|
149
|
+
size += value.length;
|
|
150
|
+
}
|
|
151
|
+
} finally {
|
|
152
|
+
await reader.cancel().catch(() => {});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const all = new Uint8Array(size);
|
|
156
|
+
let at = 0;
|
|
157
|
+
for (const c of chunks) { all.set(c, at); at += c.length; }
|
|
158
|
+
return { status: res.status, headers: Object.fromEntries(res.headers.entries()), body: all.slice(0, maxBytes + 1) };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const sleepMs = (s) => new Promise((resolve) => setTimeout(resolve, s * 1000));
|
|
162
|
+
|
|
163
|
+
export async function request(method, url, {
|
|
164
|
+
headers = {}, body = null, deadlineS = 30, retries = 2, maxBytes = DEFAULT_MAX_BYTES, backoffBaseS = 0.5,
|
|
165
|
+
maxBackoffS = 8, maxRetryAfterS = 30, insecure = false, transport = fetchTransport, sleep = sleepMs,
|
|
166
|
+
clock = () => performance.now() / 1000, rand = Math.random,
|
|
167
|
+
} = {}) {
|
|
168
|
+
method = String(method).toUpperCase();
|
|
169
|
+
if (!TOKEN.test(method)) throw new HttpError('the method is not an HTTP token', { kind: 'refused' });
|
|
170
|
+
if (!Number.isInteger(retries) || retries < 0 || retries > 10) {
|
|
171
|
+
throw new HttpError('retries is an integer from 0 to 10', { kind: 'refused' });
|
|
172
|
+
}
|
|
173
|
+
if (!(deadlineS > 0 && maxBytes > 0)) throw new HttpError('deadlineS and maxBytes are positive', { kind: 'refused' });
|
|
174
|
+
checkUrl(url, { insecure });
|
|
175
|
+
const sent = checkHeaders(headers ?? {});
|
|
176
|
+
let data = null;
|
|
177
|
+
if (body === null || body === undefined) data = null;
|
|
178
|
+
else if (body instanceof Uint8Array) data = body;
|
|
179
|
+
else if (body instanceof ArrayBuffer) data = new Uint8Array(body);
|
|
180
|
+
else if (typeof body === 'string') data = new TextEncoder().encode(body);
|
|
181
|
+
else if (typeof body === 'object') {
|
|
182
|
+
data = jsonBytes(body);
|
|
183
|
+
if (!Object.keys(sent).some((k) => k.toLowerCase() === 'content-type')) sent['Content-Type'] = 'application/json';
|
|
184
|
+
} else throw new HttpError('the body is bytes, a string, an object or an array', { kind: 'refused' });
|
|
185
|
+
const where = redactUrl(url);
|
|
186
|
+
const end = clock() + deadlineS;
|
|
187
|
+
let attempt = 0;
|
|
188
|
+
for (;;) {
|
|
189
|
+
const left = end - clock();
|
|
190
|
+
if (left <= 0) {
|
|
191
|
+
throw new HttpError(`${method} ${where}: the deadline passed before an answer`,
|
|
192
|
+
{ url: where, attempts: attempt, kind: 'timeout' });
|
|
193
|
+
}
|
|
194
|
+
attempt += 1;
|
|
195
|
+
let got = null;
|
|
196
|
+
let failure = null;
|
|
197
|
+
try {
|
|
198
|
+
got = await transport({ method, url, headers: { ...sent }, body: data, timeoutS: left, maxBytes });
|
|
199
|
+
} catch (err) {
|
|
200
|
+
if (err instanceof HttpError) throw err;
|
|
201
|
+
failure = err?.name === 'TimeoutError' || err?.name === 'AbortError'
|
|
202
|
+
? new HttpError(`${method} ${where}: timed out`, { url: where, attempts: attempt, kind: 'timeout' })
|
|
203
|
+
: new HttpError(`${method} ${where}: connection error (${err?.name ?? 'Error'})`,
|
|
204
|
+
{ url: where, attempts: attempt, kind: 'connection' });
|
|
205
|
+
}
|
|
206
|
+
let answer = null;
|
|
207
|
+
let wait;
|
|
208
|
+
if (failure === null) {
|
|
209
|
+
if (got.body.length > maxBytes) {
|
|
210
|
+
throw new HttpError(`${method} ${where}: the answer is over ${maxBytes} bytes`,
|
|
211
|
+
{ status: got.status, url: where, attempts: attempt, kind: 'too-large' });
|
|
212
|
+
}
|
|
213
|
+
answer = new Response(got.status, got.headers, got.body, { attempts: attempt, url: where });
|
|
214
|
+
if (!mayRetry(method, sent, answer.status) || attempt > retries) return answer;
|
|
215
|
+
wait = retryAfterS(answer.headers['retry-after'] ?? null);
|
|
216
|
+
if (wait !== null) {
|
|
217
|
+
if (wait > maxRetryAfterS) return answer;
|
|
218
|
+
} else {
|
|
219
|
+
wait = backoffS(attempt - 1, { baseS: backoffBaseS, maxS: maxBackoffS, rand: rand() });
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
if (!mayRetry(method, sent, null) || attempt > retries) throw failure;
|
|
223
|
+
wait = backoffS(attempt - 1, { baseS: backoffBaseS, maxS: maxBackoffS, rand: rand() });
|
|
224
|
+
}
|
|
225
|
+
if (clock() + wait >= end) {
|
|
226
|
+
if (answer !== null) return answer;
|
|
227
|
+
throw failure;
|
|
228
|
+
}
|
|
229
|
+
await sleep(wait);
|
|
230
|
+
}
|
|
231
|
+
}
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs
CHANGED
|
@@ -147,8 +147,8 @@ const ERROR_CODE = /^[a-z][a-z0-9-]{1,63}$/;
|
|
|
147
147
|
const MAX_STATUS_MESSAGE = 500;
|
|
148
148
|
const MAX_ERROR_MESSAGE = 2000;
|
|
149
149
|
const codePoints = (value) => [...value].length;
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
import { isString } from './fabric-jsvalue.mjs'; // the core's (FAA-65)
|
|
151
|
+
import { isObject } from './fabric-jsvalue.mjs'; // the core's (FAA-65)
|
|
152
152
|
const own = (value, key) => (isObject(value) && Object.hasOwn(value, key) ? value[key] : undefined);
|
|
153
153
|
|
|
154
154
|
/** A UTC instant as milliseconds, or NaN: the pattern and a day that exists, as the contract's strictTime reads it. */
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// JavaScript's view of JSON values, once for the whole Node kit (FAA-65, layer L0); the twin of fabric_jsvalue.py.
|
|
2
|
+
// The contract's answers are computed by TypeScript; every reader in the kit takes these helpers from here:
|
|
3
|
+
// isObject, isString, objects (an array's object items), indexed (with their index as written), show (the
|
|
4
|
+
// contract's `show`), toNumber (the contract's `toNumber`), keyOf, jsonEqual (the contract's `jsonEqual`) and jcs
|
|
5
|
+
// (RFC 8785). Node 20+, no dependency; nothing here imports another kit module.
|
|
6
|
+
|
|
7
|
+
/** A plain JSON object (not null, not an array). */
|
|
8
|
+
export const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
|
+
export const isString = (value) => typeof value === 'string';
|
|
10
|
+
/** The object items of an array; [] for anything else. */
|
|
11
|
+
export const objects = (value) => (Array.isArray(value) ? value.filter(isObject) : []);
|
|
12
|
+
/** The object items of an array with their index as written, so an instancePath names the item even after a non-object. */
|
|
13
|
+
export const indexed = (value) => (Array.isArray(value) ? value.flatMap((item, index) => (isObject(item) ? [[item, index]] : [])) : []);
|
|
14
|
+
|
|
15
|
+
/** The contract's `show`: String(value), or its JSON where String() throws (an object whose own `toString` is data). */
|
|
16
|
+
export function show(value) {
|
|
17
|
+
if (typeof value === 'string') return value;
|
|
18
|
+
try {
|
|
19
|
+
return String(value);
|
|
20
|
+
} catch {
|
|
21
|
+
return JSON.stringify(value) ?? typeof value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The contract's `toNumber`: Number(value), or NaN where Number() throws. */
|
|
26
|
+
export function toNumber(value) {
|
|
27
|
+
try {
|
|
28
|
+
return Number(value);
|
|
29
|
+
} catch {
|
|
30
|
+
return Number.NaN;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A collision-free key over JSON values. */
|
|
35
|
+
export const keyOf = (...parts) => JSON.stringify(parts);
|
|
36
|
+
|
|
37
|
+
/** The contract's jsonEqual: arrays in order, objects by their members whatever the order, anything else by ===. */
|
|
38
|
+
export function jsonEqual(a, b) {
|
|
39
|
+
if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, i) => jsonEqual(item, b[i]));
|
|
40
|
+
if (isObject(a) || isObject(b)) {
|
|
41
|
+
if (!isObject(a) || !isObject(b)) return false;
|
|
42
|
+
const keys = Object.keys(a);
|
|
43
|
+
return keys.length === Object.keys(b).length && keys.every((key) => Object.hasOwn(b, key) && jsonEqual(a[key], b[key]));
|
|
44
|
+
}
|
|
45
|
+
return a === b;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** RFC 8785: members sorted by UTF-16 code units, numbers in ECMAScript form; refuses what has no canonical form. */
|
|
49
|
+
export function jcs(value) {
|
|
50
|
+
if (value === null || typeof value === 'boolean') return JSON.stringify(value);
|
|
51
|
+
if (typeof value === 'number') {
|
|
52
|
+
if (!Number.isFinite(value)) throw new TypeError('a non-finite number has no canonical form');
|
|
53
|
+
return JSON.stringify(value);
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === 'string') {
|
|
56
|
+
if (/\p{Surrogate}/u.test(value)) throw new TypeError('a lone surrogate has no canonical form');
|
|
57
|
+
return JSON.stringify(value);
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(value)) return `[${Array.from(value, (item) => jcs(item)).join(',')}]`;
|
|
60
|
+
if (isObject(value)) {
|
|
61
|
+
const prototype = Object.getPrototypeOf(value);
|
|
62
|
+
if (prototype !== Object.prototype && prototype !== null) throw new TypeError('only plain JSON objects have a canonical form');
|
|
63
|
+
const keys = Object.keys(value).sort();
|
|
64
|
+
return `{${keys.map((key) => `${jcs(key)}:${jcs(value[key])}`).join(',')}}`;
|
|
65
|
+
}
|
|
66
|
+
throw new TypeError(`${typeof value} has no canonical form`);
|
|
67
|
+
}
|