Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "data-monorepo",
"version": "0.10.25",
"version": "0.10.26",
"private": true,
"engines": {
"node": ">=24"
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.25",
"version": "0.10.26",
"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.25",
"version": "0.10.26",
"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.25",
"version": "0.10.26",
"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.25",
"version": "0.10.26",
"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.25",
"version": "0.10.26",
"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.25",
"version": "0.10.26",
"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-rpc/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adobe/data-rpc",
"version": "0.10.25",
"version": "0.10.26",
"description": "Schema-driven, bidirectional projection of @adobe/data async data services across a boundary (iframe / MessagePort / Worker). Only Data crosses the wire.",
"type": "module",
"sideEffects": 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.25",
"version": "0.10.26",
"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.25",
"version": "0.10.26",
"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.25",
"version": "0.10.26",
"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.25",
"version": "0.10.26",
"description": "Adobe data oriented programming library",
"type": "module",
"sideEffects": false,
Expand Down
31 changes: 31 additions & 0 deletions packages/data/src/ecs/database/default-factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
import { describe, it, expect } from "vitest";
import { Database } from "./database.js";
import type { Schema } from "../../schema/index.js";
import type { Store } from "../store/index.js";
import { applyOperations } from "./transactional-store/apply-operations.js";
import type { Assert } from "../../types/assert.js";
import type { Equal } from "../../types/equal.js";

// `guid` stands in for a cross-runtime identity: a value the archetype must have
// at creation but that callers usually don't supply. `defaultFactory: "guid"`
Expand Down Expand Up @@ -84,3 +88,30 @@ describe("insert-time default-factory (full plugin chain)", () => {
t.archetypes.Node.insert({ guid: 2 });
};
}

// A default-factory store must stay assignable to the any-row `Store<any, any, any>`
// that generic helpers take. Otherwise a single such transaction member (e.g. the
// library's own `applyOperations`) is rejected by the plugin's transaction constraint
// and every transaction on that plugin silently vanishes from `db.transactions`.
{
const anyStorePlugin = Database.Plugin.create({
components: { guid, value },
archetypes: { Node: ["guid", "value"] } as const,
transactions: {
applyOperations,
addNode(t, args: { value: number }) {
return t.archetypes.Node.insert(args);
},
},
});
const _transactionsKept = (db: Database.FromPlugin<typeof anyStorePlugin>) => {
db.transactions.addNode({ value: 1 });
db.transactions.applyOperations([]);
};

type NodeStore = Database.Plugin.ToStore<typeof nodePlugin>;
const _assignable = (t: NodeStore): Store<any, any, any> => t;
// Store's inference helpers match a default-factory store instead of yielding `never`.
type _CheckArchetypes = Assert<Equal<Store.Archetypes<NodeStore>, { readonly Node: readonly ["guid", "value"] }>>;
type _CheckResourcesNotNever = Assert<Equal<[Store.Resources<NodeStore>] extends [never] ? true : false, false>>;
}
12 changes: 10 additions & 2 deletions packages/data/src/schema/resolve-external-invocation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ 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"];
type External = Schema["external"];

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

describe("resolveExternalInvocation", () => {
Expand All @@ -33,4 +34,11 @@ describe("resolveExternalInvocation", () => {
it("channels resolve independently", () => {
expect(resolveExternalInvocation(fn({ link: true, agent: false }))).toEqual({ link: true, agent: false });
});

it("reads the schema-level policy on any member, not just functions", () => {
const hiddenState: Schema = { type: "observe", value: { type: "number" }, external: { agent: false } };
expect(resolveExternalInvocation(hiddenState)).toEqual({ link: false, agent: false });
const linkedGroup: Schema = { type: "object", properties: {}, external: { link: true } };
expect(resolveExternalInvocation(linkedGroup)).toEqual({ link: true, agent: true });
});
});
9 changes: 4 additions & 5 deletions packages/data/src/schema/resolve-external-invocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
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
* Resolves a schema's untrusted-channel exposure 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):
Expand All @@ -19,9 +19,8 @@ import type { Schema } from "./schema.js";
* (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,
link: schema.external?.link === true,
agent: schema.external?.agent !== false,
};
}
38 changes: 17 additions & 21 deletions packages/data/src/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,28 +110,24 @@ export interface Schema {
signature?: {
readonly parameters?: readonly (Parameter | 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 };
};
/**
* Exposure policy for **untrusted channels**, valid on any schema (an action, a
* state, a nested group). For a function it governs invocation; for any other
* member it governs visibility (e.g. `toTypeScript` omits members hidden from the
* agent). Pure metadata: does NOT affect `ToType` or service-schema validation.
* Resolve it with `resolveExternalInvocation(schema)` — the single source of
* truth — rather than re-deriving per call site.
*
* The two channels have **deliberately opposite default polarity**, matching
* their trust level:
* - `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**: exposed only when `link === true`.
* - `agent` — an agent acting on the user's behalf: more trusted.
* **Default-allow blacklist**: exposed unless `agent === false`.
*/
external?: { readonly agent?: boolean; readonly link?: boolean };
properties?: { readonly [key: string]: Schema };
// Per-property metadata kept OUT of `properties` so a property whose type is a
// shared/by-reference schema can carry field-level docs without mutating that
Expand Down
6 changes: 3 additions & 3 deletions packages/data/src/schema/to-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,9 +373,9 @@ type CheckFunctionNoParams = True<EquivalentTypes<TestFunctionNoParams, () => vo
// `external` invocation-policy metadata never affects the derived function type.
type TestFunctionExternalIgnored = ToType<{
type: 'function', signature: {
parameters: [{ name: 'a', schema: { type: 'number' } }], returns: { type: 'promise', value: { type: 'number' } },
external: { link: true, agent: false }
}
parameters: [{ name: 'a', schema: { 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>>>;

Expand Down
54 changes: 54 additions & 0 deletions packages/data/src/schema/to-typescript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -821,4 +821,58 @@ describe("Schema.toTypeScript", () => {
);
});
});

describe("external audience", () => {
const point = { type: "object", properties: { x: { type: "number" } }, required: ["x"] } as const satisfies Schema;
const service = {
type: "object",
properties: {
count: { type: "observe", value: { type: "number" } },
secret: { type: "observe", value: point, external: { agent: false } },
reset: { type: "function", external: { agent: false } },
share: { type: "function", external: { link: true } },
group: {
type: "object",
properties: {
at: point,
hidden: { type: "observe", value: { type: "string" }, external: { agent: false } },
},
required: ["at", "hidden"],
},
},
required: ["count", "secret", "reset", "share", "group"],
} as const satisfies Schema;

it("omits agent-hidden members at any depth by default", () => {
// `point` appears once once `secret` is dropped, so it is no longer hoisted.
expect(Schema.toTypeScript(service, "Svc")).toBe(
[
"interface Svc {",
" readonly count: Observe<number>;",
" readonly share: () => void;",
" readonly group: {",
" readonly at: {",
" readonly x: number;",
" };",
" };",
"}",
].join("\n"),
);
});

it("link audience keeps only link-enabled members", () => {
expect(Schema.toTypeScript(service, "Svc", { audience: "link" })).toBe(
["interface Svc {", " readonly share: () => void;", "}"].join("\n"),
);
});

it("all audience emits every member", () => {
const out = Schema.toTypeScript(service, "Svc", { audience: "all" });
for (const key of ["count", "secret", "reset", "share", "group", "hidden"]) {
expect(out).toContain(`readonly ${key}`);
}
// `point` now appears twice, so it is hoisted.
expect(out.startsWith("interface At {")).toBe(true);
});
});
});
22 changes: 18 additions & 4 deletions packages/data/src/schema/to-typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { Schema } from "./schema.js";
import { isNamed, schemaOf } from "./parameter.js";
import { resolveExternalInvocation } from "./resolve-external-invocation.js";

const indent = " ";

Expand All @@ -22,6 +23,11 @@ const indent = " ";
* referencing property key or parameter name, else a generated `Shape<n>`.
* - **Emits `description`s as `//` comments** directly above the declaration, property,
* or function they annotate (one comment line per line of the description).
* - **Omits members hidden from the `audience`** (default `"agent"`): an object property
* whose schema `external` policy denies that channel (see `resolveExternalInvocation`)
* is left out entirely, at any depth. So by default a member marked
* `external: { agent: false }` is never shown to an agent. `"link"` keeps only
* `link: true` members (default-deny); `"all"` emits every member.
*
* A schema that resolves to an object is emitted as an `interface`; anything else as a
* `type` alias. The result references the ambient names `Blob`, `Promise`,
Expand Down Expand Up @@ -55,8 +61,12 @@ const indent = " ";
* // }
* ```
*/
export function toTypeScript(schema: Schema, name: string): string {
const context = createContext();
export function toTypeScript(
schema: Schema,
name: string,
options: { readonly audience?: "agent" | "link" | "all" } = {},
): string {
const context = createContext(options.audience ?? "agent");
walk(schema, undefined, 1, context);
const hoisted = assignNames(schema, name, context);

Expand All @@ -77,10 +87,13 @@ interface Context {
readonly keyHint: Map<string, string>;
// Shape key → assigned interface name, for the shapes that are hoisted + shared.
readonly names: Map<string, string>;
// Whether an object property is shown to the target audience.
readonly visible: (schema: Schema) => boolean;
}

function createContext(): Context {
function createContext(audience: "agent" | "link" | "all"): Context {
return {
visible: audience === "all" ? () => true : (schema) => resolveExternalInvocation(schema)[audience],
counts: new Map(),
discovery: [],
firstSchema: new Map(),
Expand Down Expand Up @@ -134,7 +147,7 @@ function walk(schema: Schema, keyHint: string | undefined, multiplier: number, c
if (schema.type === undefined && schema.default !== undefined) return;
if (schema.type === "object" || schema.properties !== undefined) {
for (const [key, child] of Object.entries(schema.properties ?? {})) {
walk(child, key, multiplier, context);
if (context.visible(child)) walk(child, key, multiplier, context);
}
const additional = schema.additionalProperties;
if (additional !== undefined && typeof additional === "object") {
Expand Down Expand Up @@ -296,6 +309,7 @@ function objectBody(schema: Schema, depth: number, context: Context): string {
const members: string[] = [];

for (const [key, propSchema] of Object.entries(schema.properties ?? {})) {
if (!context.visible(propSchema)) continue;
const optional = required.has(key) ? "" : "?";
// A `propertyMeta` description is the author's explicit field doc — it wins and is
// never suppressed; otherwise fall back to the property type's own description.
Expand Down
10 changes: 10 additions & 0 deletions packages/data/src/service/async-data-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,13 @@ See [create-lazy.md](./create-lazy.md) for complete documentation.
- **create-lazy.md** - Complete documentation and examples
- **public.ts** - Public API exports
- **index.ts** - Namespace export

### `AsyncDataService.toObserveState(service)`

Observes every `Observe` member a service's schema describes — recursing through nested organizational groups — in parallel, and emits one combined object with the service's nested shape, typed as `AsyncDataService.State<T>` (observe members become their values; actions and action-only groups are omitted). Nothing is emitted until every member has a value; the member set is re-derived when `service.schema` changes. The service must expose its `schema`.

```typescript
const unobserve = AsyncDataService.toObserveState(service)((state) => {
state.foo.state.b; // the current value of service.foo.state.b
});
```
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ AsyncDataService.createLazy({
- `{ type: "function", signature: { returns: { type: "promise", … } } }` → a function returning `Promise<T>`
- `{ type: "function" }` (no `signature`) → a function returning `void`

The function constructor groups its `parameters`/`returns` (and invocation-policy `external`) under a nested `signature`, so those members live only on function schemas. Use `value: {}` as a "don't-care" (resolves to `any`) when a member's precise value type doesn't matter for wrapping; fill in real value schemas when the schema is also a published contract. Function `signature.parameters` list only the **required** parameters. Publish the schema beside the service with the namespace pattern and validate it with `IsValidWithCompleteSchema`.
The function constructor groups its `parameters`/`returns` under a nested `signature`, so those members live only on function schemas. The invocation policy `external` sits on the member schema itself (valid on any member). Use `value: {}` as a "don't-care" (resolves to `any`) when a member's precise value type doesn't matter for wrapping; fill in real value schemas when the schema is also a published contract. Function `signature.parameters` list only the **required** parameters. Publish the schema beside the service with the namespace pattern and validate it with `IsValidWithCompleteSchema`.

## Type Safety Guarantees

Expand Down
Loading
Loading