Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "data-monorepo",
"version": "0.10.6",
"version": "0.10.7",
"private": true,
"engines": {
"node": ">=24"
Expand All @@ -17,7 +17,7 @@
"dev-gpu": "pnpm --parallel --filter @adobe/data --filter @adobe/data-gpu --filter data-gpu-samples run dev",
"link": "pnpm -r --filter @adobe/data* run link",
"publish": "sh -c 'for x in \"$@\"; do OTP=\"$x\"; done; export NPM_CONFIG_OTP=\"$OTP\"; pnpm -r run build && pnpm -r publish --no-git-checks --access public' sh",
"bump": "pnpm version patch --no-git-tag-version && V=$(node -p \"require('$PWD/package.json').version\") && pnpm -r exec pnpm version $V --no-git-tag-version --allow-same-version && node -e \"const fs=require('fs');const p='packages/data-ai/.claude-plugin/plugin.json';const j=JSON.parse(fs.readFileSync(p,'utf8'));j.version=process.argv[1];fs.writeFileSync(p,JSON.stringify(j,null,2)+'\\n')\" $V",
"bump": "node scripts/bump.mjs",
"release": "pnpm bump && pnpm publish",
"bp": "pnpm bump && pnpm run publish"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/data-ai/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "adobe-data-ai",
"version": "0.10.6",
"version": "0.10.7",
"description": "Architecture skills for @adobe/data — data-oriented modelling, archetype iteration, hot-path performance, and related conventions.",
"author": {
"name": "Adobe"
Expand Down
2 changes: 1 addition & 1 deletion packages/data-ai/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-ai",
"version": "0.10.6",
"version": "0.10.7",
"description": "Cross-agent architecture skills for @adobe/data — installable as a Claude Code plugin or copied into any Agent-Skills-compatible agent (Cursor, Codex).",
"type": "module",
"private": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-gpu/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-gpu",
"version": "0.10.6",
"version": "0.10.7",
"description": "Adobe data WebGPU plugins and types for graphics and compute",
"type": "module",
"private": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-lit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-lit",
"version": "0.10.6",
"version": "0.10.7",
"description": "Adobe data Lit bindings - hooks, elements, decorators",
"type": "module",
"private": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-persistence/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-persistence",
"version": "0.10.6",
"version": "0.10.7",
"description": "Worker-based incremental persistence layer for @adobe/data ECS over OPFS (browser) and node:fs (server).",
"type": "module",
"sideEffects": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-react",
"version": "0.10.6",
"version": "0.10.7",
"description": "Adobe data React bindings — hooks and context for ECS database",
"type": "module",
"private": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-solid/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-solid",
"version": "0.10.6",
"version": "0.10.7",
"description": "Adobe data SolidJS bindings — context and provider for ECS database",
"type": "module",
"private": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-sync/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-sync",
"version": "0.10.6",
"version": "0.10.7",
"description": "Multi-user real-time synchronisation for @adobe/data ECS — server, client, and in-process loopback.",
"type": "module",
"sideEffects": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data-testing/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-testing",
"version": "0.10.6",
"version": "0.10.7",
"description": "Conformance-testing utilities (Match + Conformance runners) for @adobe/data ECS features",
"type": "module",
"sideEffects": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/data/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data",
"version": "0.10.6",
"version": "0.10.7",
"description": "Adobe data oriented programming library",
"type": "module",
"sideEffects": false,
Expand Down
1 change: 1 addition & 0 deletions packages/data/src/schema/public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from "./schema.js";
export * from "./to-vertex-buffer-layout.js";
export * from "./nullable.js";
export * from "./to-type.js";
export * from "./resolve-external-invocation.js";
export * from "./from-object-properties.js";
export * from "./from-archetype.js";
export * from "./from-struct-properties.js";
Expand Down
36 changes: 36 additions & 0 deletions packages/data/src/schema/resolve-external-invocation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// © 2026 Adobe. MIT License. See /LICENSE for details.

import { describe, it, expect } from "vitest";
import { resolveExternalInvocation } from "./resolve-external-invocation.js";
import type { Schema } from "./schema.js";

type External = NonNullable<Schema["signature"]>["external"];

const fn = (external?: External): Schema => ({
type: "function",
signature: { parameters: [], ...(external ? { external } : {}) },
});

describe("resolveExternalInvocation", () => {
it("defaults: link denied, agent allowed (no external)", () => {
expect(resolveExternalInvocation(fn())).toEqual({ link: false, agent: true });
});

it("link is a default-deny whitelist — only link:true permits", () => {
expect(resolveExternalInvocation(fn({ link: true })).link).toBe(true);
expect(resolveExternalInvocation(fn({ link: false })).link).toBe(false);
// absent link (even with agent set) ⇒ denied
expect(resolveExternalInvocation(fn({ agent: true })).link).toBe(false);
});

it("agent is a default-allow blacklist — only agent:false denies", () => {
expect(resolveExternalInvocation(fn({ agent: false })).agent).toBe(false);
expect(resolveExternalInvocation(fn({ agent: true })).agent).toBe(true);
// absent agent (even with link set) ⇒ allowed
expect(resolveExternalInvocation(fn({ link: true })).agent).toBe(true);
});

it("channels resolve independently", () => {
expect(resolveExternalInvocation(fn({ link: true, agent: false }))).toEqual({ link: true, agent: false });
});
});
27 changes: 27 additions & 0 deletions packages/data/src/schema/resolve-external-invocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// © 2026 Adobe. MIT License. See /LICENSE for details.

import type { Schema } from "./schema.js";

/**
* Resolves a `function` schema's untrusted-channel invocation policy
* (`schema.external`) into plain booleans, applying the two channels' opposite
* default polarity in ONE place so call sites never re-derive it (getting the
* `=== true` vs `!== false` polarity wrong on the link channel would be a
* security hole):
*
* - `link` — the least-trusted channel (a deeplink / URL anyone can craft and get
* a victim to open in their authenticated session). Default-DENY whitelist:
* invocable only when `external.link === true`.
* - `agent` — acting on the user's behalf, more trusted. Default-ALLOW blacklist:
* invocable unless `external.agent === false`.
*
* Safe to call on any schema; a schema without a policy resolves to the defaults
* (link denied, agent allowed).
*/
export function resolveExternalInvocation(schema: Schema): { readonly link: boolean; readonly agent: boolean } {
const external = schema.signature?.external;
return {
link: external?.link === true,
agent: external?.agent !== false,
};
}
43 changes: 42 additions & 1 deletion packages/data/src/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ export type Conditional = {
value: JSONMergePatch;
}

const schemaTypes = { number: true, integer: true, string: true, boolean: true, null: true, array: true, object: true, 'typed-buffer': true, blob: true } as const;
// Data types describe serializable/storable values. The type-constructor types
// (observe/promise/generator/function) describe data-adjacent *types* — reactive
// values, async values, streams, and callables — so a Schema can describe a
// service surface, not just data. Like `blob`/`typed-buffer`, these are confined
// by usage: they belong in service/interface schemas, never in ECS component,
// resource, or typed-buffer schemas (which handle-or-throw at runtime, as today).
const schemaTypes = { number: true, integer: true, string: true, boolean: true, null: true, array: true, object: true, 'typed-buffer': true, blob: true, observe: true, promise: true, generator: true, function: true } as const;

export interface Schema {
type?: keyof typeof schemaTypes;
Expand Down Expand Up @@ -60,6 +66,41 @@ export interface Schema {
minItems?: number;
maxItems?: number;
items?: Schema;
// The wrapped value type for the `observe`/`promise`/`generator` constructors:
// `{ type: "observe", value: S }` → `Observe<ToType<S>>`, etc. Absent ⇒ any.
value?: Schema;
/**
* The signature of the `function` constructor, grouped so these members live
* only on function schemas rather than on every `Schema`:
* `{ type: "function", signature: { parameters, returns } }` →
* `(...args) => ToType<returns>`. Absent `parameters` ⇒ no args; absent
* `returns` ⇒ void; absent `signature` entirely ⇒ `() => void`.
*/
signature?: {
readonly parameters?: readonly Schema[];
readonly returns?: Schema;
/**
* Invocation policy — who may call this function from an **untrusted
* channel**. Read at runtime by the executor that performs the invocation;
* pure metadata that does NOT affect the type produced by `Schema.ToType`
* (a function differing only in `external` derives the same signature), nor
* service-schema validation or lazy wrapping.
*
* The two channels have **deliberately opposite default polarity**, matching
* their trust level. Resolve them with `resolveExternalInvocation(schema)`
* (see `resolve-external-invocation.ts`) — the single source of truth —
* rather than re-deriving per call site, because getting the `link` default
* wrong is a security hole.
*
* - `link` — a deeplink / URL: the least-trusted channel (anyone can craft a
* URL and get a victim to open it in their authenticated session).
* **Default-deny whitelist**: link-invocable only when `link === true`;
* absent or `false` ⇒ not link-invocable.
* - `agent` — an agent acting on the user's behalf: more trusted.
* **Default-allow blacklist**: agent-invocable unless `agent === false`.
*/
readonly external?: { readonly agent?: boolean; readonly link?: boolean };
};
properties?: { readonly [key: string]: Schema };
required?: readonly string[];
additionalProperties?: boolean | Schema;
Expand Down
109 changes: 108 additions & 1 deletion packages/data/src/schema/to-type.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// © 2026 Adobe. MIT License. See /LICENSE for details.

import { TypedBuffer } from "../typed-buffer/typed-buffer.js";
import { DeepReadonly, EquivalentTypes, True } from "../types/types.js";
import { DeepReadonly, EquivalentTypes, False, True } from "../types/types.js";
import type { Observe } from "../observe/index.js";
import { Schema } from "./schema.js";

export type ToType<T, Depth extends number = 5> =
Expand All @@ -26,6 +27,16 @@ type FromSchemaInternal<T, Depth extends number = 5> = T extends { const: infer
? boolean
: T extends { type: 'null' }
? null
: T extends { type: 'blob' }
? Blob
: T extends { type: 'observe' }
? Observe<ToType<ValueSchema<T>, Decrement<Depth>>>
: T extends { type: 'promise' }
? Promise<ToType<ValueSchema<T>, Decrement<Depth>>>
: T extends { type: 'generator' }
? AsyncGenerator<ToType<ValueSchema<T>, Decrement<Depth>>>
: T extends { type: 'function' }
? FromSchemaFunction<T, Decrement<Depth>>
: T extends { type: 'typed-buffer', items: infer Items }
? TypedBuffer<FromSchemaInternal<Items>>
: T extends { type: 'typed-buffer' }
Expand All @@ -45,6 +56,28 @@ type Decrement<N extends number> = ((...x: any[]) => void) extends (
? R['length']
: never;

// The wrapped value schema for observe/promise/generator; absent ⇒ any.
type ValueSchema<T> = T extends { value: infer V } ? V : {};

// The function-constructor mapping: `signature.parameters` → positional args,
// `signature.returns` → result. Absent `signature` ⇒ `() => void`.
type FromSchemaFunction<T, Depth extends number> =
T extends { signature: infer Sig }
? (...args: FromSchemaArgs<SignatureParams<Sig>, Depth>) => FromSchemaReturns<Sig, Depth>
: () => void;

// The signature's parameters tuple; absent ⇒ no args.
type SignatureParams<Sig> =
Sig extends { parameters: infer P } ? P extends readonly Schema[] ? P : readonly [] : readonly [];

type FromSchemaArgs<P extends readonly Schema[], Depth extends number> = {
-readonly [K in keyof P]: ToType<P[K], Depth>;
};

// Absent `returns` ⇒ void (a function that returns nothing meaningful).
type FromSchemaReturns<Sig, Depth extends number> =
Sig extends { returns: infer R } ? R extends Schema ? ToType<R, Depth> : void : void;

type FromSchemaArray<T, Depth extends number> = T extends {
items: infer Items;
}
Expand Down Expand Up @@ -264,3 +297,77 @@ type TestAllOfSingle = ToType<{
}>; // { name?: string }
type CheckAllOfSingle = True<EquivalentTypes<TestAllOfSingle, { name?: string }>>;

// ============================================================================
// TYPE-CONSTRUCTOR SCHEMAS (data-adjacent types)
// ============================================================================

// observe
type TestObserve = ToType<{ type: 'observe', value: { type: 'number' } }>; // Observe<number>
type CheckObserve = True<EquivalentTypes<TestObserve, Observe<number>>>;

type TestObserveNoValue = ToType<{ type: 'observe' }>; // Observe<any>
type CheckObserveNoValue = True<EquivalentTypes<TestObserveNoValue, Observe<any>>>;

type TestObserveObject = ToType<{
type: 'observe',
value: { type: 'object', properties: { x: { type: 'number' } }, required: ['x'], additionalProperties: false }
}>; // Observe<{ readonly x: number }>
type CheckObserveObject = True<EquivalentTypes<TestObserveObject, Observe<{ readonly x: number }>>>;

// promise
type TestPromise = ToType<{ type: 'promise', value: { type: 'string' } }>; // Promise<string>
type CheckPromise = True<EquivalentTypes<TestPromise, Promise<string>>>;

// generator
type TestGenerator = ToType<{ type: 'generator', value: { type: 'boolean' } }>; // AsyncGenerator<boolean>
type CheckGenerator = True<EquivalentTypes<TestGenerator, AsyncGenerator<boolean>>>;

// function
type TestFunction = ToType<{
type: 'function', signature: { parameters: [{ type: 'number' }, { type: 'string' }], returns: { type: 'boolean' } }
}>; // (a: number, b: string) => boolean
type CheckFunction = True<EquivalentTypes<TestFunction, (a: number, b: string) => boolean>>;

type TestFunctionVoid = ToType<{ type: 'function', signature: { parameters: [] } }>; // () => void
type CheckFunctionVoid = True<EquivalentTypes<TestFunctionVoid, () => void>>;

type TestFunctionNoParams = ToType<{ type: 'function' }>; // () => void (no signature)
type CheckFunctionNoParams = True<EquivalentTypes<TestFunctionNoParams, () => void>>;

// `external` invocation-policy metadata never affects the derived function type.
type TestFunctionExternalIgnored = ToType<{
type: 'function', signature: {
parameters: [{ type: 'number' }], returns: { type: 'promise', value: { type: 'number' } },
external: { link: true, agent: false }
}
}>; // (a: number) => Promise<number>
type CheckFunctionExternalIgnored = True<EquivalentTypes<TestFunctionExternalIgnored, (a: number) => Promise<number>>>;

// Driver case: an AsyncGenerator<Data> parameter nested inside an object argument,
// with a Promise return — the shape plain-Data parameters could not express.
type TestStreamingAction = ToType<{
type: 'function',
signature: {
parameters: [{
type: 'object',
properties: { chunks: { type: 'generator', value: { type: 'number' } } },
required: ['chunks'],
additionalProperties: false
}],
returns: { type: 'promise', value: { type: 'null' } }
}
}>; // (arg: { readonly chunks: AsyncGenerator<number> }) => Promise<null>
type CheckStreamingAction = True<EquivalentTypes<
TestStreamingAction,
(arg: { readonly chunks: AsyncGenerator<number> }) => Promise<null>
>>;

// Negatives — inner types, constructor kinds, and function signatures are honored.
type CheckObserveNotString = False<EquivalentTypes<TestObserve, Observe<string>>>;
type CheckObserveNotPromise = False<EquivalentTypes<TestObserve, Promise<number>>>;
type CheckPromiseNotObserve = False<EquivalentTypes<TestPromise, Observe<string>>>;
type CheckGeneratorNotPromise = False<EquivalentTypes<TestGenerator, Promise<boolean>>>;
type CheckFunctionWrongArg = False<EquivalentTypes<TestFunction, (a: string, b: string) => boolean>>;
type CheckFunctionWrongReturn = False<EquivalentTypes<TestFunction, (a: number, b: string) => number>>;
type CheckFunctionWrongArity = False<EquivalentTypes<TestFunction, (a: number) => boolean>>;

Loading