@danypops/vehicle-core 0.17.0 → 0.18.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/README.md +12 -0
- package/dist/{vehicle-approvals.d.ts → approvals/approval.d.ts} +4 -11
- package/dist/{vehicle-approvals.js → approvals/approval.js} +33 -23
- package/dist/approvals/index.d.ts +1 -0
- package/dist/approvals/index.js +1 -0
- package/dist/client/client.d.ts +29 -0
- package/dist/client/client.js +1 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/content/content.d.ts +32 -0
- package/dist/content/content.js +25 -0
- package/dist/content/index.d.ts +1 -0
- package/dist/content/index.js +1 -0
- package/dist/{vehicle-errors.d.ts → errors/error.d.ts} +10 -9
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/events/event.d.ts +42 -0
- package/dist/events/event.js +35 -0
- package/dist/events/index.d.ts +1 -0
- package/dist/events/index.js +1 -0
- package/dist/idempotency/index.d.ts +1 -0
- package/dist/idempotency/index.js +1 -0
- package/dist/{vehicle-idempotency.d.ts → idempotency/receipt.d.ts} +1 -1
- package/dist/index.d.ts +26 -8
- package/dist/index.js +26 -8
- package/dist/jobs/grant.d.ts +36 -0
- package/dist/jobs/grant.js +31 -0
- package/dist/jobs/identity.d.ts +11 -0
- package/dist/jobs/identity.js +13 -0
- package/dist/jobs/index.d.ts +8 -0
- package/dist/jobs/index.js +8 -0
- package/dist/jobs/replay.d.ts +8 -0
- package/dist/jobs/replay.js +9 -0
- package/dist/jobs/retention.d.ts +26 -0
- package/dist/jobs/retention.js +39 -0
- package/dist/jobs/steer.d.ts +23 -0
- package/dist/jobs/steer.js +41 -0
- package/dist/jobs/termination.d.ts +5 -0
- package/dist/jobs/termination.js +11 -0
- package/dist/jobs/wake-log.d.ts +39 -0
- package/dist/jobs/wake-log.js +65 -0
- package/dist/jobs/wire.d.ts +47 -0
- package/dist/jobs/wire.js +1 -0
- package/dist/manifest/index.d.ts +1 -0
- package/dist/manifest/index.js +1 -0
- package/dist/manifest/manifest.d.ts +49 -0
- package/dist/manifest/manifest.js +1 -0
- package/dist/operations/context.d.ts +52 -0
- package/dist/operations/context.js +1 -0
- package/dist/operations/effect.d.ts +3 -0
- package/dist/operations/effect.js +2 -0
- package/dist/operations/index.d.ts +3 -0
- package/dist/operations/index.js +3 -0
- package/dist/operations/operation.d.ts +98 -0
- package/dist/operations/operation.js +79 -0
- package/dist/{atomic-json.d.ts → persistence/atomic-json.d.ts} +1 -2
- package/dist/{atomic-json.js → persistence/atomic-json.js} +1 -2
- package/dist/persistence/index.d.ts +1 -0
- package/dist/persistence/index.js +1 -0
- package/dist/schedules/index.d.ts +1 -0
- package/dist/schedules/index.js +1 -0
- package/dist/{vehicle-scheduler.d.ts → schedules/schedule.d.ts} +6 -8
- package/dist/schemas/codec.d.ts +25 -0
- package/dist/schemas/codec.js +7 -0
- package/dist/schemas/index.d.ts +4 -0
- package/dist/schemas/index.js +4 -0
- package/dist/schemas/json.d.ts +7 -0
- package/dist/schemas/json.js +14 -0
- package/dist/schemas/loose-object.d.ts +20 -0
- package/dist/schemas/loose-object.js +73 -0
- package/dist/schemas/presentation.d.ts +11 -0
- package/dist/schemas/presentation.js +22 -0
- package/dist/watches/index.d.ts +1 -0
- package/dist/watches/index.js +1 -0
- package/dist/{vehicle-watchers.d.ts → watches/registry.d.ts} +2 -9
- package/dist/{vehicle-watchers.js → watches/registry.js} +2 -9
- package/package.json +5 -1
- package/src/{vehicle-approvals.ts → approvals/approval.ts} +27 -24
- package/src/approvals/index.ts +1 -0
- package/src/client/client.ts +31 -0
- package/src/client/index.ts +1 -0
- package/src/content/content.ts +46 -0
- package/src/content/index.ts +1 -0
- package/src/{vehicle-errors.ts → errors/error.ts} +10 -9
- package/src/errors/index.ts +1 -0
- package/src/events/event.ts +72 -0
- package/src/events/index.ts +1 -0
- package/src/idempotency/index.ts +1 -0
- package/src/{vehicle-idempotency.ts → idempotency/receipt.ts} +1 -1
- package/src/index.ts +26 -8
- package/src/jobs/grant.ts +53 -0
- package/src/jobs/identity.ts +13 -0
- package/src/jobs/index.ts +8 -0
- package/src/jobs/replay.ts +17 -0
- package/src/jobs/retention.ts +59 -0
- package/src/jobs/steer.ts +49 -0
- package/src/jobs/termination.ts +13 -0
- package/src/jobs/wake-log.ts +98 -0
- package/src/jobs/wire.ts +51 -0
- package/src/manifest/index.ts +1 -0
- package/src/manifest/manifest.ts +52 -0
- package/src/operations/context.ts +56 -0
- package/src/operations/effect.ts +3 -0
- package/src/operations/index.ts +3 -0
- package/src/operations/operation.ts +184 -0
- package/src/{atomic-json.ts → persistence/atomic-json.ts} +1 -2
- package/src/persistence/index.ts +1 -0
- package/src/schedules/index.ts +1 -0
- package/src/{vehicle-scheduler.ts → schedules/schedule.ts} +6 -8
- package/src/schemas/codec.ts +30 -0
- package/src/schemas/index.ts +4 -0
- package/src/schemas/json.ts +17 -0
- package/src/schemas/loose-object.ts +82 -0
- package/src/schemas/presentation.ts +28 -0
- package/src/watches/index.ts +1 -0
- package/src/{vehicle-watchers.ts → watches/registry.ts} +2 -9
- package/dist/vehicle-contract.d.ts +0 -353
- package/dist/vehicle-contract.js +0 -253
- package/dist/vehicle-jobs.d.ts +0 -159
- package/dist/vehicle-jobs.js +0 -182
- package/src/vehicle-contract.ts +0 -597
- package/src/vehicle-jobs.ts +0 -305
- /package/dist/{vehicle-errors.js → errors/error.js} +0 -0
- /package/dist/{vehicle-idempotency.js → idempotency/receipt.js} +0 -0
- /package/dist/{vehicle-scheduler.js → schedules/schedule.js} +0 -0
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* "Trigger something at a future time or on a recurring interval,"
|
|
3
3
|
* independent of any specific job's own lifecycle -- a distinct shape from
|
|
4
|
-
* Vehicle Jobs (submit-then-track a unit of work already running).
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* bespoke callback closure, so it can be persisted and re-armed after a
|
|
9
|
-
* restart the way a closure never could.
|
|
4
|
+
* Vehicle Jobs (submit-then-track a unit of work already running). The
|
|
5
|
+
* fired action is a declarative Vehicle operation invocation or event
|
|
6
|
+
* emission -- never a bespoke callback closure, so it can be persisted and
|
|
7
|
+
* re-armed after a restart the way a closure never could.
|
|
10
8
|
*
|
|
11
9
|
* Pure pieces only: trigger/action shapes and the fire-time arithmetic.
|
|
12
10
|
* Real timers, persistence, and registry wiring live in vehicle-server's
|
|
13
|
-
* VehicleScheduler, the same core/server split
|
|
11
|
+
* VehicleScheduler, the same core/server split Vehicle Jobs uses.
|
|
14
12
|
*/
|
|
15
|
-
import type { JsonValue } from "
|
|
13
|
+
import type { JsonValue } from "../schemas/json.js";
|
|
16
14
|
export type VehicleScheduleTrigger = {
|
|
17
15
|
readonly kind: "at";
|
|
18
16
|
readonly at: number;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type JsonSchema } from "./json.js";
|
|
2
|
+
export interface VehicleSchemaIssue {
|
|
3
|
+
readonly path: readonly (string | number)[];
|
|
4
|
+
readonly message: string;
|
|
5
|
+
}
|
|
6
|
+
export type VehicleSchemaResult<T> = {
|
|
7
|
+
readonly success: true;
|
|
8
|
+
readonly value: T;
|
|
9
|
+
} | {
|
|
10
|
+
readonly success: false;
|
|
11
|
+
readonly issues?: readonly VehicleSchemaIssue[];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* A serializable, descriptive `jsonSchema` (surfaced to a client or Pi tool
|
|
15
|
+
* projection) paired with a real `safeParse` that actually enforces it at
|
|
16
|
+
* runtime -- a Vehicle registry's own `invoke()` only ever calls
|
|
17
|
+
* `safeParse`; `jsonSchema` alone is never itself enforced, so a codec that
|
|
18
|
+
* only sets `jsonSchema` without a matching `safeParse` is a documentation
|
|
19
|
+
* gesture, not an honest contract.
|
|
20
|
+
*/
|
|
21
|
+
export interface VehicleSchemaCodec<T> {
|
|
22
|
+
readonly jsonSchema: JsonSchema;
|
|
23
|
+
safeParse(value: unknown): VehicleSchemaResult<T>;
|
|
24
|
+
}
|
|
25
|
+
export declare function defineVehicleSchema<T>(codec: VehicleSchemaCodec<T>): VehicleSchemaCodec<T>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
2
|
+
export type JsonValue = JsonPrimitive | readonly JsonValue[] | {
|
|
3
|
+
readonly [key: string]: JsonValue;
|
|
4
|
+
};
|
|
5
|
+
export type JsonSchema = Readonly<Record<string, JsonValue>>;
|
|
6
|
+
export declare function cloneJson<T extends JsonValue>(value: T): T;
|
|
7
|
+
export declare function freezeJson(value: JsonValue): JsonValue;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function cloneJson(value) {
|
|
2
|
+
const serialized = JSON.stringify(value);
|
|
3
|
+
if (serialized === undefined)
|
|
4
|
+
throw new Error("Vehicle JSON metadata must be serializable");
|
|
5
|
+
return freezeJson(JSON.parse(serialized));
|
|
6
|
+
}
|
|
7
|
+
export function freezeJson(value) {
|
|
8
|
+
if (Array.isArray(value))
|
|
9
|
+
return Object.freeze(value.map(freezeJson));
|
|
10
|
+
if (value !== null && typeof value === "object") {
|
|
11
|
+
return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, child]) => [key, freezeJson(child)])));
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type VehicleSchemaCodec } from "./codec.js";
|
|
2
|
+
export interface LooseObjectProperty {
|
|
3
|
+
readonly type: string;
|
|
4
|
+
readonly enum?: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A VehicleRegistry only ever calls a schema's own safeParse -- jsonSchema is
|
|
8
|
+
* descriptive metadata surfaced to a client/Pi projection, never itself
|
|
9
|
+
* enforced at runtime -- so a declared `type`/`enum`/`additionalProperties: false`
|
|
10
|
+
* has to be checked here for real, or it's a documentation gesture, not an
|
|
11
|
+
* honest contract (the exact drift this function's own jsonSchema metadata
|
|
12
|
+
* had before: it always advertised `additionalProperties: false` and a
|
|
13
|
+
* per-property `type`, while safeParse only ever checked `required` and
|
|
14
|
+
* `enum`). Every consumer projecting a plain-object input onto a
|
|
15
|
+
* VehicleOperation needs the same required/type/extra-key/enum checks; this
|
|
16
|
+
* is that check written once.
|
|
17
|
+
*/
|
|
18
|
+
export declare function defineLooseObjectSchema(properties: Record<string, LooseObjectProperty>, required?: readonly string[]): VehicleSchemaCodec<Record<string, unknown>>;
|
|
19
|
+
/** Accepts any value unvalidated -- for an operation whose output shape isn't worth a dedicated schema (an internal/low-stakes result, or one already validated upstream by the domain logic it wraps). */
|
|
20
|
+
export declare const passthroughVehicleSchema: VehicleSchemaCodec<unknown>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { defineVehicleSchema } from "./codec.js";
|
|
2
|
+
/** JSON Schema's own `type` keyword vocabulary -- checked for real below so a declared `type: "number"` (say) can't silently accept a string forever. `"integer"` additionally requires no fractional part, matching JSON Schema's own distinction from plain `"number"`. An unrecognized type name is treated as "anything goes" (matches passthroughVehicleSchema's own precedent for a shape not worth strictly enforcing) rather than rejecting every input outright for what would otherwise be a schema-authoring typo. */
|
|
3
|
+
function matchesLooseObjectPropertyType(type, value) {
|
|
4
|
+
switch (type) {
|
|
5
|
+
case "string":
|
|
6
|
+
return typeof value === "string";
|
|
7
|
+
case "number":
|
|
8
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
9
|
+
case "integer":
|
|
10
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
11
|
+
case "boolean":
|
|
12
|
+
return typeof value === "boolean";
|
|
13
|
+
case "object":
|
|
14
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
case "array":
|
|
16
|
+
return Array.isArray(value);
|
|
17
|
+
case "null":
|
|
18
|
+
return value === null;
|
|
19
|
+
default:
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A VehicleRegistry only ever calls a schema's own safeParse -- jsonSchema is
|
|
25
|
+
* descriptive metadata surfaced to a client/Pi projection, never itself
|
|
26
|
+
* enforced at runtime -- so a declared `type`/`enum`/`additionalProperties: false`
|
|
27
|
+
* has to be checked here for real, or it's a documentation gesture, not an
|
|
28
|
+
* honest contract (the exact drift this function's own jsonSchema metadata
|
|
29
|
+
* had before: it always advertised `additionalProperties: false` and a
|
|
30
|
+
* per-property `type`, while safeParse only ever checked `required` and
|
|
31
|
+
* `enum`). Every consumer projecting a plain-object input onto a
|
|
32
|
+
* VehicleOperation needs the same required/type/extra-key/enum checks; this
|
|
33
|
+
* is that check written once.
|
|
34
|
+
*/
|
|
35
|
+
export function defineLooseObjectSchema(properties, required = []) {
|
|
36
|
+
return defineVehicleSchema({
|
|
37
|
+
// LooseObjectProperty's named fields (type, enum) are all JSON-value-shaped
|
|
38
|
+
// at runtime, but TypeScript's structural check against the recursive
|
|
39
|
+
// JsonValue union doesn't see that through a plain interface -- the cast
|
|
40
|
+
// is a type-system limitation, not a runtime concern.
|
|
41
|
+
jsonSchema: { type: "object", properties: properties, required: [...required], additionalProperties: false },
|
|
42
|
+
safeParse(value) {
|
|
43
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
44
|
+
return { success: false, issues: [{ path: [], message: "input must be an object" }] };
|
|
45
|
+
}
|
|
46
|
+
const input = value;
|
|
47
|
+
for (const key of required) {
|
|
48
|
+
if (!(key in input))
|
|
49
|
+
return { success: false, issues: [{ path: [key], message: `${key} is required` }] };
|
|
50
|
+
}
|
|
51
|
+
for (const key of Object.keys(input)) {
|
|
52
|
+
if (!(key in properties))
|
|
53
|
+
return { success: false, issues: [{ path: [key], message: `${key} is not a recognized property` }] };
|
|
54
|
+
}
|
|
55
|
+
for (const [key, schema] of Object.entries(properties)) {
|
|
56
|
+
if (!(key in input))
|
|
57
|
+
continue;
|
|
58
|
+
if (!matchesLooseObjectPropertyType(schema.type, input[key])) {
|
|
59
|
+
return { success: false, issues: [{ path: [key], message: `${key} must be of type ${schema.type}` }] };
|
|
60
|
+
}
|
|
61
|
+
if (schema.enum && !schema.enum.includes(input[key])) {
|
|
62
|
+
return { success: false, issues: [{ path: [key], message: `${key} must be one of ${schema.enum.join(", ")}` }] };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { success: true, value: input };
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** Accepts any value unvalidated -- for an operation whose output shape isn't worth a dedicated schema (an internal/low-stakes result, or one already validated upstream by the domain logic it wraps). */
|
|
70
|
+
export const passthroughVehicleSchema = defineVehicleSchema({
|
|
71
|
+
jsonSchema: { type: "object" },
|
|
72
|
+
safeParse: (value) => ({ success: true, value }),
|
|
73
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Shared defense-in-depth vocabulary for credential-shaped fields at every Vehicle projection boundary. */
|
|
2
|
+
export declare const VEHICLE_CREDENTIAL_FIELD_NAMES: readonly ["password", "token", "accessToken", "refreshToken", "apiKey", "secret", "authorization", "credential"];
|
|
3
|
+
/** Case/separator-insensitive credential-name check used as a fallback when a schema annotation is missing. */
|
|
4
|
+
export declare function isVehicleCredentialFieldName(name: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* JSON Schema property annotation consumed by human-facing Vehicle adapters.
|
|
7
|
+
* `omit` hides the field; `summarize` may show shape/size but never its value.
|
|
8
|
+
* Standard `writeOnly: true` and `format: "password"` always imply omission.
|
|
9
|
+
*/
|
|
10
|
+
export declare const VEHICLE_SCHEMA_PRESENTATION_EXTENSION: "x-vehicle-presentation";
|
|
11
|
+
export type VehicleSchemaPresentation = "omit" | "summarize";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Shared defense-in-depth vocabulary for credential-shaped fields at every Vehicle projection boundary. */
|
|
2
|
+
export const VEHICLE_CREDENTIAL_FIELD_NAMES = Object.freeze([
|
|
3
|
+
"password",
|
|
4
|
+
"token",
|
|
5
|
+
"accessToken",
|
|
6
|
+
"refreshToken",
|
|
7
|
+
"apiKey",
|
|
8
|
+
"secret",
|
|
9
|
+
"authorization",
|
|
10
|
+
"credential",
|
|
11
|
+
]);
|
|
12
|
+
const NORMALIZED_VEHICLE_CREDENTIAL_FIELD_NAMES = new Set(VEHICLE_CREDENTIAL_FIELD_NAMES.map((name) => name.replace(/[^a-z0-9]/gi, "").toLowerCase()));
|
|
13
|
+
/** Case/separator-insensitive credential-name check used as a fallback when a schema annotation is missing. */
|
|
14
|
+
export function isVehicleCredentialFieldName(name) {
|
|
15
|
+
return NORMALIZED_VEHICLE_CREDENTIAL_FIELD_NAMES.has(name.replace(/[^a-z0-9]/gi, "").toLowerCase());
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* JSON Schema property annotation consumed by human-facing Vehicle adapters.
|
|
19
|
+
* `omit` hides the field; `summarize` may show shape/size but never its value.
|
|
20
|
+
* Standard `writeOnly: true` and `format: "password"` always imply omission.
|
|
21
|
+
*/
|
|
22
|
+
export const VEHICLE_SCHEMA_PRESENTATION_EXTENSION = "x-vehicle-presentation";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./registry.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./registry.js";
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* "Watch a changing resource, get notified" --
|
|
3
|
-
*
|
|
4
|
-
* generalized past Lector's own workspace/pattern vocabulary (`workspaceId`
|
|
5
|
-
* -> `scope`, `pattern` -> `resource`) into a shared Vehicle primitive.
|
|
6
|
-
* Confirmed independently reinvented three-plus times across this house's
|
|
7
|
-
* own ecosystem before this existed: Lector's own registry, Lector's CLI
|
|
8
|
-
* (a second, non-resilient reimplementation of the same watch/subscribe
|
|
9
|
-
* shape), and Papyrus's TaskOverlay/NoteOverlay (each hand-rolling the same
|
|
10
|
-
* ensurePushChannel()+poll dance independently).
|
|
2
|
+
* "Watch a changing resource, get notified" -- a shared Vehicle primitive
|
|
3
|
+
* generalizing a (scope, resource) watch registration into a wire topic.
|
|
11
4
|
*
|
|
12
5
|
* Pure, in-memory bookkeeping only -- no filesystem, network, or PushChannel
|
|
13
6
|
* I/O here. Matching pattern/resource against a real changed resource is a
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* "Watch a changing resource, get notified" --
|
|
3
|
-
*
|
|
4
|
-
* generalized past Lector's own workspace/pattern vocabulary (`workspaceId`
|
|
5
|
-
* -> `scope`, `pattern` -> `resource`) into a shared Vehicle primitive.
|
|
6
|
-
* Confirmed independently reinvented three-plus times across this house's
|
|
7
|
-
* own ecosystem before this existed: Lector's own registry, Lector's CLI
|
|
8
|
-
* (a second, non-resilient reimplementation of the same watch/subscribe
|
|
9
|
-
* shape), and Papyrus's TaskOverlay/NoteOverlay (each hand-rolling the same
|
|
10
|
-
* ensurePushChannel()+poll dance independently).
|
|
2
|
+
* "Watch a changing resource, get notified" -- a shared Vehicle primitive
|
|
3
|
+
* generalizing a (scope, resource) watch registration into a wire topic.
|
|
11
4
|
*
|
|
12
5
|
* Pure, in-memory bookkeeping only -- no filesystem, network, or PushChannel
|
|
13
6
|
* I/O here. Matching pattern/resource against a real changed resource is a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/vehicle-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Vehicle's runtime-neutral wire contract: operation descriptors, schema codecs, failure shapes. Zero runtime dependencies, zero Bun-specific code -- the one thing every Vehicle client and server package depends on.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
".": {
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./persistence": {
|
|
15
|
+
"types": "./dist/persistence/index.d.ts",
|
|
16
|
+
"default": "./dist/persistence/index.js"
|
|
13
17
|
}
|
|
14
18
|
},
|
|
15
19
|
"scripts": {
|
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
* node:crypto and lives in vehicle-server instead, the same split
|
|
7
7
|
* atomic-json.ts already uses for fs access.
|
|
8
8
|
*/
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
9
|
+
import { defineVehicleEvent } from "../events/event.js";
|
|
10
|
+
import type { VehiclePrincipal } from "../operations/context.js";
|
|
11
|
+
import type { VehicleEffect } from "../operations/effect.js";
|
|
12
|
+
import { VEHICLE_EFFECTS } from "../operations/effect.js";
|
|
13
|
+
import { defineVehicleSchema } from "../schemas/codec.js";
|
|
11
14
|
|
|
12
15
|
/** A sha256 hex digest: exactly 64 lowercase hex characters -- matches hashApprovalInput's own (vehicle-server) output shape. */
|
|
13
16
|
const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
|
|
@@ -19,9 +22,9 @@ function isVehicleEffect(value: unknown): value is VehicleEffect {
|
|
|
19
22
|
function isVehiclePrincipal(value: unknown): value is VehiclePrincipal {
|
|
20
23
|
if (typeof value !== "object" || value === null) return false;
|
|
21
24
|
const candidate = value as Record<string, unknown>;
|
|
22
|
-
if (typeof candidate
|
|
23
|
-
if (candidate
|
|
24
|
-
return typeof candidate
|
|
25
|
+
if (typeof candidate["id"] !== "string") return false;
|
|
26
|
+
if (candidate["claims"] === undefined) return true;
|
|
27
|
+
return typeof candidate["claims"] === "object" && candidate["claims"] !== null && !Array.isArray(candidate["claims"]);
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
/** Set once at registry-configuration time (VehicleRegistry.configureApprovals()); never on a per-invoke basis. */
|
|
@@ -102,22 +105,22 @@ const requestedPayloadSchema = defineVehicleSchema<VehicleApprovalRequest>({
|
|
|
102
105
|
if (typeof value !== "object" || value === null) return { success: false, issues: [{ path: [], message: "input must be an object" }] };
|
|
103
106
|
const row = value as Record<string, unknown>;
|
|
104
107
|
if (
|
|
105
|
-
typeof row
|
|
106
|
-
typeof row
|
|
107
|
-
typeof row
|
|
108
|
-
!Number.isInteger(row
|
|
109
|
-
row
|
|
110
|
-
!isVehicleEffect(row
|
|
111
|
-
typeof row
|
|
112
|
-
!Number.isFinite(row
|
|
113
|
-
typeof row
|
|
114
|
-
!Number.isFinite(row
|
|
115
|
-
typeof row
|
|
116
|
-
!SHA256_HEX_PATTERN.test(row
|
|
108
|
+
typeof row["requestId"] !== "string" ||
|
|
109
|
+
typeof row["operationName"] !== "string" ||
|
|
110
|
+
typeof row["operationVersion"] !== "number" ||
|
|
111
|
+
!Number.isInteger(row["operationVersion"]) ||
|
|
112
|
+
row["operationVersion"] < 1 ||
|
|
113
|
+
!isVehicleEffect(row["effect"]) ||
|
|
114
|
+
typeof row["requestedAt"] !== "number" ||
|
|
115
|
+
!Number.isFinite(row["requestedAt"]) ||
|
|
116
|
+
typeof row["expiresAt"] !== "number" ||
|
|
117
|
+
!Number.isFinite(row["expiresAt"]) ||
|
|
118
|
+
typeof row["inputHash"] !== "string" ||
|
|
119
|
+
!SHA256_HEX_PATTERN.test(row["inputHash"])
|
|
117
120
|
) {
|
|
118
121
|
return { success: false, issues: [{ path: [], message: "invalid approval request payload" }] };
|
|
119
122
|
}
|
|
120
|
-
if (row
|
|
123
|
+
if (row["principal"] !== undefined && !isVehiclePrincipal(row["principal"])) {
|
|
121
124
|
return { success: false, issues: [{ path: ["principal"], message: "invalid approval request principal" }] };
|
|
122
125
|
}
|
|
123
126
|
return { success: true, value: row as unknown as VehicleApprovalRequest };
|
|
@@ -141,17 +144,17 @@ const resolvedPayloadSchema = defineVehicleSchema<VehicleApprovalOutcome>({
|
|
|
141
144
|
if (typeof value !== "object" || value === null) return { success: false, issues: [{ path: [], message: "input must be an object" }] };
|
|
142
145
|
const row = value as Record<string, unknown>;
|
|
143
146
|
if (
|
|
144
|
-
typeof row
|
|
145
|
-
(row
|
|
146
|
-
typeof row
|
|
147
|
-
!Number.isFinite(row
|
|
147
|
+
typeof row["requestId"] !== "string" ||
|
|
148
|
+
(row["decision"] !== "granted" && row["decision"] !== "denied") ||
|
|
149
|
+
typeof row["decidedAt"] !== "number" ||
|
|
150
|
+
!Number.isFinite(row["decidedAt"])
|
|
148
151
|
) {
|
|
149
152
|
return { success: false, issues: [{ path: [], message: "invalid approval outcome payload" }] };
|
|
150
153
|
}
|
|
151
|
-
if (row
|
|
154
|
+
if (row["decidedBy"] !== undefined && typeof row["decidedBy"] !== "string") {
|
|
152
155
|
return { success: false, issues: [{ path: ["decidedBy"], message: "decidedBy must be a string" }] };
|
|
153
156
|
}
|
|
154
|
-
if (row
|
|
157
|
+
if (row["comment"] !== undefined && typeof row["comment"] !== "string") {
|
|
155
158
|
return { success: false, issues: [{ path: ["comment"], message: "comment must be a string" }] };
|
|
156
159
|
}
|
|
157
160
|
return { success: true, value: row as unknown as VehicleApprovalOutcome };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./approval.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { VehicleJobSnapshot, VehicleJobSubmitOptions, VehicleJobSubmitResult, VehicleJobTailResult } from "../jobs/wire.js";
|
|
2
|
+
import type { VehicleManifest } from "../manifest/manifest.js";
|
|
3
|
+
import type { VehicleInvocationOptions } from "../operations/context.js";
|
|
4
|
+
|
|
5
|
+
export interface VehicleSubscription {
|
|
6
|
+
close(): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface VehicleClient {
|
|
10
|
+
manifest(): Promise<VehicleManifest>;
|
|
11
|
+
invoke<Output = unknown>(name: string, version: number, input: unknown, options?: VehicleInvocationOptions): Promise<Output>;
|
|
12
|
+
close(): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Vehicle Jobs -- submit a background-capable operation (one whose descriptor declares
|
|
15
|
+
* `background`, see {@link VehicleBackgroundCapability}) and get its jobId back immediately,
|
|
16
|
+
* without waiting for the operation itself to make any progress. Optional: a client that
|
|
17
|
+
* never talks to a job-capable Vehicle (or a hand-rolled test double) simply omits these five
|
|
18
|
+
* methods, exactly like this interface's own long-standing `subscribe()`-shaped extras --
|
|
19
|
+
* present on both LocalVehicleClient and RemoteVehicleClient, absent elsewhere. Feature-detect
|
|
20
|
+
* via the operation's own manifest `background` capability, not by probing for these methods.
|
|
21
|
+
*/
|
|
22
|
+
submitJob?(name: string, version: number, input: unknown, options?: VehicleJobSubmitOptions): Promise<VehicleJobSubmitResult>;
|
|
23
|
+
/** Never blocks -- current status, plus output/error once terminal. */
|
|
24
|
+
pollJob?(jobId: string): Promise<VehicleJobSnapshot>;
|
|
25
|
+
/** Progress entries strictly after `cursor` (0 for everything so far), plus the next cursor. Never blocks. */
|
|
26
|
+
tailJob?(jobId: string, cursor?: number): Promise<VehicleJobTailResult>;
|
|
27
|
+
/** Pushes new input to an already-running job's handler, if it opted in via context.steerInputs. */
|
|
28
|
+
steerJob?(jobId: string, input: unknown): Promise<void>;
|
|
29
|
+
/** Best-effort cancellation of a still-running job -- a no-op against an already-terminal one. */
|
|
30
|
+
cancelJob?(jobId: string): Promise<void>;
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./client.js";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A block of narrative text meant to be read by the model, not parsed as
|
|
3
|
+
* data -- same field name and shape MCP's own CallToolResult.content and
|
|
4
|
+
* Pi's own ToolDefinition.execute() return already use, so a Vehicle
|
|
5
|
+
* operation adopting this needs no translation layer at either boundary.
|
|
6
|
+
* Only the "text" variant exists here; there's no Vehicle use case yet for
|
|
7
|
+
* MCP's image/audio/resource-link block kinds.
|
|
8
|
+
*/
|
|
9
|
+
export interface VehicleContentBlock {
|
|
10
|
+
readonly type: "text";
|
|
11
|
+
readonly text: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* An operation's Output type can intersect this to carry its own
|
|
16
|
+
* model-facing narrative alongside its structured data, e.g.
|
|
17
|
+
* `type RunOutput = { runId: string; created: Task[] } & WithVehicleContent`.
|
|
18
|
+
* The operation itself builds `content` since it's the only code that
|
|
19
|
+
* actually knows how to describe what it computed -- never a per-consumer
|
|
20
|
+
* override bolted on wherever the operation happens to get registered.
|
|
21
|
+
*/
|
|
22
|
+
export interface WithVehicleContent {
|
|
23
|
+
readonly content?: readonly VehicleContentBlock[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Reads an operation's own `content` blocks off its output when present and
|
|
28
|
+
* well-formed, so a generic Vehicle client can prefer them over dumping raw
|
|
29
|
+
* JSON at the model -- without knowing anything about the operation's own
|
|
30
|
+
* domain shape. Returns undefined for a malformed or absent `content` field;
|
|
31
|
+
* the caller falls back to its own default (formatted JSON) rather than
|
|
32
|
+
* risk forwarding partial/garbled blocks.
|
|
33
|
+
*/
|
|
34
|
+
export function extractVehicleContent(output: unknown): readonly VehicleContentBlock[] | undefined {
|
|
35
|
+
if (typeof output !== "object" || output === null || Array.isArray(output)) return undefined;
|
|
36
|
+
const content = (output as { readonly content?: unknown }).content;
|
|
37
|
+
if (!Array.isArray(content) || content.length === 0) return undefined;
|
|
38
|
+
const blocks: VehicleContentBlock[] = [];
|
|
39
|
+
for (const block of content) {
|
|
40
|
+
if (typeof block !== "object" || block === null) return undefined;
|
|
41
|
+
const { type, text } = block as { readonly type?: unknown; readonly text?: unknown };
|
|
42
|
+
if (type !== "text" || typeof text !== "string") return undefined;
|
|
43
|
+
blocks.push({ type: "text", text });
|
|
44
|
+
}
|
|
45
|
+
return blocks;
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./content.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { VehicleSchemaIssue } from "../schemas/codec.js";
|
|
2
|
+
import type { JsonValue } from "../schemas/json.js";
|
|
2
3
|
|
|
3
4
|
const VEHICLE_ERROR_BRAND = Symbol.for("@danypops/vehicle-core/VehicleError");
|
|
4
5
|
|
|
@@ -55,10 +56,10 @@ export interface VehicleFailure {
|
|
|
55
56
|
export interface VehicleErrorOptions {
|
|
56
57
|
readonly category: VehicleFailureCategory;
|
|
57
58
|
readonly retryable?: boolean;
|
|
58
|
-
readonly retryAfterMs?: number;
|
|
59
|
-
readonly recovery?: VehicleRecovery;
|
|
60
|
-
readonly details?: JsonValue;
|
|
61
|
-
readonly operationId?: string;
|
|
59
|
+
readonly retryAfterMs?: number | undefined;
|
|
60
|
+
readonly recovery?: VehicleRecovery | undefined;
|
|
61
|
+
readonly details?: JsonValue | undefined;
|
|
62
|
+
readonly operationId?: string | undefined;
|
|
62
63
|
readonly cause?: unknown;
|
|
63
64
|
/** Includes cause's message in toFailure().causeMessage. Default false -- an arbitrary cause could carry a credential or internal detail. */
|
|
64
65
|
readonly exposeCause?: boolean;
|
|
@@ -113,10 +114,10 @@ export function defineErrorMapping(
|
|
|
113
114
|
export class VehicleError extends Error {
|
|
114
115
|
readonly category: VehicleFailureCategory;
|
|
115
116
|
readonly retryable: boolean;
|
|
116
|
-
readonly retryAfterMs?: number;
|
|
117
|
-
readonly recovery?: VehicleRecovery;
|
|
118
|
-
readonly details?: JsonValue;
|
|
119
|
-
readonly operationId?: string;
|
|
117
|
+
readonly retryAfterMs?: number | undefined;
|
|
118
|
+
readonly recovery?: VehicleRecovery | undefined;
|
|
119
|
+
readonly details?: JsonValue | undefined;
|
|
120
|
+
readonly operationId?: string | undefined;
|
|
120
121
|
private readonly exposeCause: boolean;
|
|
121
122
|
|
|
122
123
|
constructor(
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./error.js";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { VehicleSchemaCodec } from "../schemas/codec.js";
|
|
2
|
+
import { cloneJson, type JsonSchema } from "../schemas/json.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A named, schema'd event type a provider declares as part of its
|
|
6
|
+
* manifest -- the typed alternative to a raw PushChannel.publish(topic,
|
|
7
|
+
* payload) call with a hand-invented topic string. No `available` flag the
|
|
8
|
+
* way an operation has one: an event type, once declared, is always
|
|
9
|
+
* emittable -- there's no credential-gated "this event is currently
|
|
10
|
+
* unavailable" concept the way a live external-service-backed operation
|
|
11
|
+
* can have.
|
|
12
|
+
*/
|
|
13
|
+
export interface VehicleEventDescriptor {
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly version: number;
|
|
16
|
+
readonly description: string;
|
|
17
|
+
readonly payloadSchema: JsonSchema;
|
|
18
|
+
/** Same bounded-resource discipline as an operation's own maxRequestBytes/maxResponseBytes -- required, never silently defaulted. */
|
|
19
|
+
readonly maxPayloadBytes: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface VehicleEvent<Payload> {
|
|
23
|
+
readonly descriptor: VehicleEventDescriptor;
|
|
24
|
+
readonly payload: VehicleSchemaCodec<Payload>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DefineVehicleEventOptions<Payload> {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly version: number;
|
|
30
|
+
readonly description: string;
|
|
31
|
+
readonly payload: VehicleSchemaCodec<Payload>;
|
|
32
|
+
readonly maxPayloadBytes: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function validateEventMetadata<Payload>(options: DefineVehicleEventOptions<Payload>): void {
|
|
36
|
+
if (!options.name.trim()) throw new Error("Vehicle event name must not be empty");
|
|
37
|
+
if (!Number.isInteger(options.version) || options.version < 1) {
|
|
38
|
+
throw new Error("Vehicle event version must be a positive integer");
|
|
39
|
+
}
|
|
40
|
+
if (!options.description.trim()) throw new Error("Vehicle event description must not be empty");
|
|
41
|
+
if (!Number.isSafeInteger(options.maxPayloadBytes) || options.maxPayloadBytes < 1) {
|
|
42
|
+
throw new Error("Vehicle event maxPayloadBytes must be a positive integer");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function defineVehicleEvent<Payload>(options: DefineVehicleEventOptions<Payload>): VehicleEvent<Payload> {
|
|
47
|
+
validateEventMetadata(options);
|
|
48
|
+
const descriptor: VehicleEventDescriptor = Object.freeze({
|
|
49
|
+
name: options.name,
|
|
50
|
+
version: options.version,
|
|
51
|
+
description: options.description,
|
|
52
|
+
payloadSchema: cloneJson(options.payload.jsonSchema),
|
|
53
|
+
maxPayloadBytes: options.maxPayloadBytes,
|
|
54
|
+
});
|
|
55
|
+
return Object.freeze({ descriptor, payload: options.payload });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type VehicleManifestEvent = VehicleEventDescriptor;
|
|
59
|
+
|
|
60
|
+
export type VehicleEventHandler<Payload> = (payload: Payload) => void;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The wire topic name a bridge (bridgeVehicleEventsToPushChannel, in
|
|
64
|
+
* vehicle-server) publishes an event under, and a subscriber
|
|
65
|
+
* (RemoteVehicleClient.subscribe()) subscribes to -- one shared naming
|
|
66
|
+
* function in vehicle-core so both sides can never drift apart on the
|
|
67
|
+
* convention, the same failure mode this primitive exists to prevent
|
|
68
|
+
* providers from reinventing per-project.
|
|
69
|
+
*/
|
|
70
|
+
export function vehicleEventTopic(name: string, version: number): string {
|
|
71
|
+
return `vehicle-event:${name}@${version}`;
|
|
72
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./event.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./receipt.js";
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* VehicleJobStore split (a pure, independently-testable bounded-retention rule here; the stateful
|
|
6
6
|
* store that calls it lives in vehicle-server).
|
|
7
7
|
*/
|
|
8
|
-
import type { VehicleFailure } from "
|
|
8
|
+
import type { VehicleFailure } from "../errors/error.js";
|
|
9
9
|
|
|
10
10
|
/** A settled keyed-idempotency outcome -- exactly what gets replayed to a caller reusing the same key. Never carries the original request's raw input (only its hash is ever retained, see VehicleIdempotencyReceipt) and never a credential: `output`/`failure` are already what the operation would hand back to any caller, the same wire-safe boundary VehicleJobPersistedRecord's own `output`/`error` fields already cross. */
|
|
11
11
|
export type VehicleIdempotencyResult =
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Vehicle's runtime-neutral wire contract, organized by capability (see each
|
|
3
|
+
* subfolder's own index.ts) rather than one flat forest of vehicle-*.ts files:
|
|
4
|
+
* schemas (codecs, loose-object validation, credential presentation), content
|
|
5
|
+
* (model-facing narrative blocks), operations (descriptors, effect
|
|
6
|
+
* classification, invocation context), events, manifest, client (the port a
|
|
7
|
+
* caller programs against), approvals (the Approval Gate's wire shapes), jobs
|
|
8
|
+
* (Vehicle Jobs' pure pieces), schedules, watches, and persistence (atomic
|
|
9
|
+
* JSON -- a technical utility, not a Vehicle protocol capability, kept
|
|
10
|
+
* distinct for that reason). Every symbol below is re-exported unchanged
|
|
11
|
+
* from its historical flat-file home, so root-level `import { X } from
|
|
12
|
+
* "@danypops/vehicle-core"` usage is completely unaffected by this layout.
|
|
13
|
+
*/
|
|
14
|
+
export * from "./approvals/index.js";
|
|
15
|
+
export * from "./client/index.js";
|
|
16
|
+
export * from "./content/index.js";
|
|
17
|
+
export * from "./errors/index.js";
|
|
18
|
+
export * from "./events/index.js";
|
|
19
|
+
export * from "./idempotency/index.js";
|
|
20
|
+
export * from "./jobs/index.js";
|
|
21
|
+
export * from "./manifest/index.js";
|
|
22
|
+
export * from "./operations/index.js";
|
|
23
|
+
export * from "./persistence/index.js";
|
|
24
|
+
export * from "./schedules/index.js";
|
|
25
|
+
export * from "./schemas/index.js";
|
|
26
|
+
export * from "./watches/index.js";
|