@antasphere/chassis-contract 1.0.0 → 1.0.2

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/src/wire.ts DELETED
@@ -1,246 +0,0 @@
1
- import { z } from 'zod';
2
- import {
3
- BILLING_PLANS,
4
- USAGE_CHECK_REASONS,
5
- USAGE_EVENT_MAX_FUTURE_MS,
6
- USAGE_EVENT_MAX_PAST_MS,
7
- USAGE_EVENTS_BATCH_MAX,
8
- USAGE_REJECT_REASONS,
9
- USAGE_ROUTE_ERRORS,
10
- USAGE_WRITE_SCOPE,
11
- entitlementProfileSchema,
12
- usageCheckRequestSchema,
13
- usageCheckSchema,
14
- usageEventOccurrenceIssue,
15
- usageEventSchema,
16
- usageEventsBatchSchema,
17
- usageIngestResultSchema
18
- } from './entitlements.js';
19
-
20
- /**
21
- * The chassis's side of the hub–tool wire check (PRDCT-2677). The hub owns
22
- * the messages the tools exchange with it and publishes them as ONE
23
- * generated snapshot, `packages/contract/wire/hub-tool-messages.json` of the
24
- * hub, built by its `packages/contract/src/wire.ts`. The block below, from
25
- * `WIRE_SCHEMA_KEYS` to `buildWireSnapshot`, is the chassis's copy of that
26
- * builder, kept VERBATIM so both sides compute the same picture: the same
27
- * JSON Schema export, the same probe verdicts, the same occurrence table.
28
- * The hub's inputs (its offsets and probe payloads) and its definitions are
29
- * NOT copied: the inputs are read back from the hub's file
30
- * (`wireInputsOf`), the definitions are this package's own copies
31
- * (`CHASSIS_WIRE_DEFINITIONS`, from `entitlements.ts`).
32
- *
33
- * `compareWireSnapshots` names every difference by its dotted path;
34
- * `scripts/hub-wire-check.mjs` (`pnpm --filter @antasphere/chassis-contract
35
- * wire:check`, the `hub-wire` CI job against the hub's `dev`) fails on any.
36
- * The check runs one way: the hub changes first, the chassis follows.
37
- */
38
-
39
- // ── Copied VERBATIM from the hub's packages/contract/src/wire.ts ──────────
40
-
41
- /** The keys of the six shapes, the same on both sides. */
42
- export const WIRE_SCHEMA_KEYS = [
43
- 'usageEvent',
44
- 'usageEventsBatch',
45
- 'usageIngestResult',
46
- 'usageEntitlements',
47
- 'usageCheckRequest',
48
- 'usageCheck'
49
- ] as const;
50
- export type WireSchemaKey = (typeof WIRE_SCHEMA_KEYS)[number];
51
-
52
- export interface WireConstants {
53
- USAGE_WRITE_SCOPE: string;
54
- BILLING_PLANS: readonly string[];
55
- USAGE_EVENTS_BATCH_MAX: number;
56
- USAGE_EVENT_MAX_PAST_MS: number;
57
- USAGE_EVENT_MAX_FUTURE_MS: number;
58
- USAGE_CHECK_REASONS: readonly string[];
59
- USAGE_REJECT_REASONS: readonly string[];
60
- USAGE_ROUTE_ERRORS: Readonly<Record<string, number>>;
61
- }
62
-
63
- export interface WireProbe {
64
- name: string;
65
- input: unknown;
66
- }
67
-
68
- export interface WireProbeResult extends WireProbe {
69
- /** Whether the schema accepts the input. */
70
- ok: boolean;
71
- }
72
-
73
- export interface WireOccurrence {
74
- /** The event's `occurredAt` minus the receipt time, in milliseconds. */
75
- offsetMs: number;
76
- issue: { path: 'occurredAt'; message: string } | null;
77
- }
78
-
79
- /** What one side feeds the builder: its own definitions. */
80
- export interface WireDefinitions {
81
- constants: WireConstants;
82
- schemas: Record<WireSchemaKey, z.ZodType>;
83
- occurrenceIssue: (occurredAt: Date, receivedAt: Date) => { path: 'occurredAt'; message: string } | null;
84
- }
85
-
86
- /** The inputs the snapshot is computed on: the hub's, read back by the chassis from the hub's file. */
87
- export interface WireInputs {
88
- occurrenceOffsetsMs: readonly number[];
89
- probes: Record<WireSchemaKey, readonly WireProbe[]>;
90
- }
91
-
92
- export interface HubToolWireSnapshot {
93
- v: 1;
94
- constants: WireConstants;
95
- schemas: Record<WireSchemaKey, unknown>;
96
- occurrence: WireOccurrence[];
97
- probes: Record<WireSchemaKey, WireProbeResult[]>;
98
- }
99
-
100
- /** A fixed receipt time, so the occurrence table is the same on every run. */
101
- const RECEIVED_AT = new Date('2026-09-23T12:00:00.000Z');
102
-
103
- /**
104
- * Build the snapshot from one side's definitions on the given inputs. The
105
- * SAME function runs on both sides (the chassis carries a verbatim copy):
106
- * the hub with its own inputs, the chassis with the inputs read from the
107
- * hub's file. Deterministic: no clock, no randomness, no environment.
108
- */
109
- export function buildWireSnapshot(defs: WireDefinitions, inputs: WireInputs): HubToolWireSnapshot {
110
- const schemas = {} as Record<WireSchemaKey, unknown>;
111
- const probes = {} as Record<WireSchemaKey, WireProbeResult[]>;
112
- for (const key of WIRE_SCHEMA_KEYS) {
113
- const schema = defs.schemas[key];
114
- // A refinement has no JSON Schema form and is dropped on both sides
115
- // alike; the probes below carry what it decides.
116
- schemas[key] = z.toJSONSchema(schema, { unrepresentable: 'any' });
117
- probes[key] = inputs.probes[key].map((probe) => ({
118
- name: probe.name,
119
- input: probe.input,
120
- ok: schema.safeParse(probe.input).success
121
- }));
122
- }
123
- return {
124
- v: 1,
125
- constants: {
126
- USAGE_WRITE_SCOPE: defs.constants.USAGE_WRITE_SCOPE,
127
- BILLING_PLANS: [...defs.constants.BILLING_PLANS],
128
- USAGE_EVENTS_BATCH_MAX: defs.constants.USAGE_EVENTS_BATCH_MAX,
129
- USAGE_EVENT_MAX_PAST_MS: defs.constants.USAGE_EVENT_MAX_PAST_MS,
130
- USAGE_EVENT_MAX_FUTURE_MS: defs.constants.USAGE_EVENT_MAX_FUTURE_MS,
131
- USAGE_CHECK_REASONS: [...defs.constants.USAGE_CHECK_REASONS],
132
- USAGE_REJECT_REASONS: [...defs.constants.USAGE_REJECT_REASONS],
133
- USAGE_ROUTE_ERRORS: { ...defs.constants.USAGE_ROUTE_ERRORS }
134
- },
135
- schemas,
136
- occurrence: inputs.occurrenceOffsetsMs.map((offsetMs) => ({
137
- offsetMs,
138
- issue: defs.occurrenceIssue(new Date(RECEIVED_AT.getTime() + offsetMs), RECEIVED_AT)
139
- })),
140
- probes
141
- };
142
- }
143
-
144
- // ── The chassis's own half ─────────────────────────────────────────────────
145
-
146
- /** The chassis's definitions: this package's copies of the hub's shapes, under the hub's keys. */
147
- export const CHASSIS_WIRE_DEFINITIONS: WireDefinitions = {
148
- constants: {
149
- USAGE_WRITE_SCOPE,
150
- BILLING_PLANS,
151
- USAGE_EVENTS_BATCH_MAX,
152
- USAGE_EVENT_MAX_PAST_MS,
153
- USAGE_EVENT_MAX_FUTURE_MS,
154
- USAGE_CHECK_REASONS,
155
- USAGE_REJECT_REASONS,
156
- USAGE_ROUTE_ERRORS
157
- },
158
- schemas: {
159
- usageEvent: usageEventSchema,
160
- usageEventsBatch: usageEventsBatchSchema,
161
- usageIngestResult: usageIngestResultSchema,
162
- usageEntitlements: entitlementProfileSchema,
163
- usageCheckRequest: usageCheckRequestSchema,
164
- usageCheck: usageCheckSchema
165
- },
166
- occurrenceIssue: usageEventOccurrenceIssue
167
- };
168
-
169
- /** The inputs the hub computed its snapshot on, read back from its file: the offsets and the probe payloads. */
170
- export function wireInputsOf(reference: HubToolWireSnapshot): WireInputs {
171
- const probes = {} as Record<WireSchemaKey, readonly WireProbe[]>;
172
- for (const key of WIRE_SCHEMA_KEYS) {
173
- probes[key] = (reference.probes[key] ?? []).map((probe) => ({ name: probe.name, input: probe.input }));
174
- }
175
- return {
176
- occurrenceOffsetsMs: reference.occurrence.map((row) => row.offsetMs),
177
- probes
178
- };
179
- }
180
-
181
- /** The snapshot the chassis's copies give on the hub's own inputs: what must equal the hub's file. */
182
- export function chassisWireSnapshot(reference: HubToolWireSnapshot): HubToolWireSnapshot {
183
- return buildWireSnapshot(CHASSIS_WIRE_DEFINITIONS, wireInputsOf(reference));
184
- }
185
-
186
- /** One place where the hub's snapshot and the chassis's differ: its dotted path and the two values. */
187
- export interface WireDifference {
188
- path: string;
189
- hub: unknown;
190
- chassis: unknown;
191
- }
192
-
193
- function isPlainObject(value: unknown): value is Record<string, unknown> {
194
- return typeof value === 'object' && value !== null && !Array.isArray(value);
195
- }
196
-
197
- function childPath(parent: string, key: string): string {
198
- return /^[A-Za-z_$][\w$]*$/.test(key)
199
- ? parent === ''
200
- ? key
201
- : `${parent}.${key}`
202
- : `${parent}[${JSON.stringify(key)}]`;
203
- }
204
-
205
- function compareAt(path: string, hub: unknown, chassis: unknown, out: WireDifference[]): void {
206
- if (Array.isArray(hub) && Array.isArray(chassis)) {
207
- if (hub.length !== chassis.length) {
208
- out.push({ path, hub, chassis });
209
- return;
210
- }
211
- for (let i = 0; i < hub.length; i += 1) compareAt(`${path}[${i}]`, hub[i], chassis[i], out);
212
- return;
213
- }
214
- if (isPlainObject(hub) && isPlainObject(chassis)) {
215
- const keys = [...new Set([...Object.keys(hub), ...Object.keys(chassis)])].sort();
216
- for (const key of keys) compareAt(childPath(path, key), hub[key], chassis[key], out);
217
- return;
218
- }
219
- if (!Object.is(hub, chassis)) out.push({ path, hub, chassis });
220
- }
221
-
222
- /**
223
- * Every difference between the hub's snapshot and the chassis's, by dotted
224
- * path (`constants.USAGE_CHECK_REASONS[2]`,
225
- * `schemas.usageCheck.properties.reason.anyOf[0].enum`,
226
- * `probes.usageCheck[3].ok`). Objects compare by the union of their keys,
227
- * order ignored, an absent key and `undefined` being the same; arrays by
228
- * index, a length difference being one difference at the array's path.
229
- * Deterministic: keys sorted, arrays in order. Empty when they agree.
230
- */
231
- export function compareWireSnapshots(
232
- hub: HubToolWireSnapshot,
233
- chassis: HubToolWireSnapshot
234
- ): WireDifference[] {
235
- const out: WireDifference[] = [];
236
- compareAt('', hub, chassis, out);
237
- return out;
238
- }
239
-
240
- /** One line of figures for a snapshot: `6 shapes, N constants, M probes, K occurrence offsets`. */
241
- export function describeWireSnapshot(snapshot: HubToolWireSnapshot): string {
242
- const shapes = Object.keys(snapshot.schemas).length;
243
- const constants = Object.keys(snapshot.constants).length;
244
- const probes = Object.values(snapshot.probes).reduce((n, list) => n + list.length, 0);
245
- return `${shapes} shapes, ${constants} constants, ${probes} probes, ${snapshot.occurrence.length} occurrence offsets`;
246
- }
@@ -1,114 +0,0 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { describe, expect, it } from 'vitest';
5
- import {
6
- buildWireSnapshot,
7
- CHASSIS_WIRE_DEFINITIONS,
8
- chassisWireSnapshot,
9
- compareWireSnapshots,
10
- type HubToolWireSnapshot,
11
- type WireInputs
12
- } from '../src/index.js';
13
-
14
- /**
15
- * The hub–tool wire check (PRDCT-2677). First the compare itself, on a
16
- * snapshot the chassis builds from its own copies, so it is proven without
17
- * any hub file; then the cross-check against the hub's snapshot, found the
18
- * way `scripts/hub-wire-check.mjs` finds it (`HUB_WIRE_SNAPSHOT`, else the
19
- * hub checkout beside this repository), skipped when neither is there. CI
20
- * runs the script against the hub's `dev` (the `hub-wire` job).
21
- */
22
-
23
- const ACCOUNT = '0b6c7c1e-4f4e-4a7b-9c1d-2e3f4a5b6c7d';
24
- const INPUTS: WireInputs = {
25
- occurrenceOffsetsMs: [0, 5 * 60 * 1000 + 1],
26
- probes: {
27
- usageEvent: [{ name: 'empty', input: {} }],
28
- usageEventsBatch: [{ name: 'empty batch', input: { events: [] } }],
29
- usageIngestResult: [
30
- { name: 'no results', input: { results: [], accepted: 0, duplicate: 0, rejected: 0 } }
31
- ],
32
- usageEntitlements: [{ name: 'missing everything', input: {} }],
33
- usageCheckRequest: [
34
- { name: 'plain', input: { accountRef: ACCOUNT, actionKey: 'things.publish', quantity: 1 } }
35
- ],
36
- usageCheck: [
37
- {
38
- name: 'unpriceable',
39
- input: {
40
- accountRef: ACCOUNT,
41
- actionKey: 'things.publish',
42
- quantity: 1,
43
- allowed: false,
44
- credits: Number.MAX_SAFE_INTEGER,
45
- balance: 0,
46
- unit: 'call',
47
- priced: true,
48
- plan: 'free',
49
- reason: 'unpriceable',
50
- topUpUrl: 'https://account.example/billing/top-up'
51
- }
52
- }
53
- ]
54
- }
55
- };
56
-
57
- const clone = (s: HubToolWireSnapshot): HubToolWireSnapshot => JSON.parse(JSON.stringify(s));
58
-
59
- describe('compareWireSnapshots', () => {
60
- const base = clone(buildWireSnapshot(CHASSIS_WIRE_DEFINITIONS, INPUTS));
61
-
62
- it('finds nothing between a snapshot and itself, and the chassis rebuilds it on its own inputs', () => {
63
- expect(compareWireSnapshots(base, clone(base))).toEqual([]);
64
- expect(compareWireSnapshots(base, clone(chassisWireSnapshot(base)))).toEqual([]);
65
- });
66
-
67
- it('names a constant, a probe verdict and a schema leaf by their paths', () => {
68
- const other = clone(base);
69
- (other.constants.USAGE_CHECK_REASONS as string[])[2] = 'over_quota';
70
- other.probes.usageCheck[0]!.ok = !other.probes.usageCheck[0]!.ok;
71
- const reason = (
72
- other.schemas.usageCheck as { properties: { reason: { anyOf: Array<{ enum?: string[] }> } } }
73
- ).properties.reason.anyOf[0]!;
74
- reason.enum = ['insufficient_credits', 'account_suspended'];
75
- const differences = compareWireSnapshots(base, other);
76
- expect(differences.map((d) => d.path)).toEqual([
77
- 'constants.USAGE_CHECK_REASONS[2]',
78
- 'probes.usageCheck[0].ok',
79
- 'schemas.usageCheck.properties.reason.anyOf[0].enum'
80
- ]);
81
- expect(differences[0]).toEqual({
82
- path: 'constants.USAGE_CHECK_REASONS[2]',
83
- hub: 'unpriceable',
84
- chassis: 'over_quota'
85
- });
86
- });
87
-
88
- it('treats an absent key as undefined and names an added one', () => {
89
- const other = clone(base);
90
- (other.constants.USAGE_ROUTE_ERRORS as Record<string, number | undefined>).payment_required = undefined;
91
- expect(compareWireSnapshots(base, other)).toEqual([]);
92
- (other.constants.USAGE_ROUTE_ERRORS as Record<string, number>).payment_required = 402;
93
- expect(compareWireSnapshots(base, other)).toEqual([
94
- { path: 'constants.USAGE_ROUTE_ERRORS.payment_required', hub: undefined, chassis: 402 }
95
- ]);
96
- });
97
- });
98
-
99
- // The hub's file, found as the script finds it: from this package, four levels up is labs/products/antasphere/.
100
- const PACKAGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
101
- const SIBLING = path.resolve(PACKAGE_DIR, '../../../../hub/packages/contract/wire/hub-tool-messages.json');
102
- const HUB_FILE = process.env.HUB_WIRE_SNAPSHOT ? path.resolve(process.env.HUB_WIRE_SNAPSHOT) : SIBLING;
103
-
104
- describe('the chassis’s copies against the hub’s wire snapshot', () => {
105
- if (!existsSync(HUB_FILE)) {
106
- it.skip(`skipped: no hub snapshot at ${HUB_FILE} (set HUB_WIRE_SNAPSHOT; CI runs the hub-wire job)`, () => {});
107
- return;
108
- }
109
- it(`agree with ${HUB_FILE}`, () => {
110
- const hub = JSON.parse(readFileSync(HUB_FILE, 'utf8')) as HubToolWireSnapshot;
111
- const differences = compareWireSnapshots(hub, clone(chassisWireSnapshot(hub)));
112
- expect(differences, JSON.stringify(differences, null, 2)).toEqual([]);
113
- });
114
- });