esoul-sdk 0.25.0 → 0.25.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/CHANGELOG.md +28 -0
- package/api-reference.md +51 -3
- package/dist/server.d.ts +50 -0
- package/dist/server.js +9 -0
- package/dist/types.d.ts +15 -0
- package/docs/06-server.md +28 -0
- package/docs/17-editing-and-merging.md +22 -0
- package/llms-full.txt +50 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
Releases before 0.20.0 are recorded in the repository history only.
|
|
4
4
|
|
|
5
|
+
**Versions say whether an app must change** (from the release after 0.25.0). A *breaking* release
|
|
6
|
+
moves the leftmost non-zero number (0.25.x → 0.26.0; after 1.0, 1.x → 2.0.0) and lists every change under
|
|
7
|
+
`### Breaking`: one bullet per API, the API in backticks first, then what an app must do. Every
|
|
8
|
+
other release — features, fixes — moves the last number. The Forge reads this: a compatible update
|
|
9
|
+
is offered with one Update button; a breaking one is reviewed by the agent against the app before
|
|
10
|
+
anything is installed. The release check refuses a version that does not match its notes
|
|
11
|
+
(src/lib/forge/release-notes.pure.ts).
|
|
12
|
+
|
|
13
|
+
## 0.25.2
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `EventDefinition.resolveConcurrent(state, eventData, { theirsSess })`: an event that writes a PATCH (sub-items with the version each built on — a CAD edit's per-node sets with their bases, a notes page's paragraphs) merges a concurrent write itself before it becomes a conflict. Called by the fence only when the write would land as a conflict; returns the rewritten event, with `detail` naming what could not be merged (kept on the conflict marker for the app's own resolution UI), or `null` to keep the plain conflict. The hook block notes merges paragraphs with, now declarable by any SDK app (docs/17 §4).
|
|
18
|
+
|
|
19
|
+
## 0.25.1
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `headlessBridge(ctx, { url, boot, calls, viewport })` (esoul-sdk/server): a web page opened in a
|
|
24
|
+
headless browser ON THE PLATFORM and driven over `postMessage`, for an app whose engine only
|
|
25
|
+
exists as a page (a WebAssembly CAD kernel, a canvas renderer). The person's open tab is one
|
|
26
|
+
place such a page can run; this is the other — on Vercel, with nobody's tab open, so an agent on
|
|
27
|
+
a phone or over MCP gets the same work done. The page is loaded top-level and is its own
|
|
28
|
+
`parent`, so an iframe bridge answers unchanged; the browser and the loaded page stay warm on the
|
|
29
|
+
platform between calls for the same url (the first call pays the launch, later ones only the
|
|
30
|
+
calls). `url` must be https and public; at most 32 calls; a Forge box has no browser, so there
|
|
31
|
+
the call throws and says so (docs/06-server.md, "The headless bridge").
|
|
32
|
+
|
|
5
33
|
## 0.25.0
|
|
6
34
|
|
|
7
35
|
### Added
|
package/api-reference.md
CHANGED
|
@@ -1812,6 +1812,11 @@ interface EventDefinition<StateType extends ApplicationIdentifier> {
|
|
|
1812
1812
|
conflictScope?: (eventData: any) => string[] | null | undefined;
|
|
1813
1813
|
merge?: MergeDescription;
|
|
1814
1814
|
baseMerge?: BaseMerge;
|
|
1815
|
+
resolveConcurrent?: (
|
|
1816
|
+
state: any,
|
|
1817
|
+
eventData: any,
|
|
1818
|
+
ctx: { theirsSess: string | null },
|
|
1819
|
+
) => { eventData: any; detail?: unknown } | null;
|
|
1815
1820
|
}
|
|
1816
1821
|
```
|
|
1817
1822
|
|
|
@@ -2515,11 +2520,11 @@ interface UsesDecl {
|
|
|
2515
2520
|
```
|
|
2516
2521
|
|
|
2517
2522
|
==============================================================================
|
|
2518
|
-
## `esoul-sdk/server` —
|
|
2523
|
+
## `esoul-sdk/server` — 119 exports
|
|
2519
2524
|
|
|
2520
2525
|
Server code only (server.ts, ops, routes, tasks): the viewer, the app's database, files, connections, machines, charts, route tokens.
|
|
2521
2526
|
|
|
2522
|
-
### Functions and values (
|
|
2527
|
+
### Functions and values (37)
|
|
2523
2528
|
|
|
2524
2529
|
#### `APPROVAL_WAIT` — const · src/computer.ts
|
|
2525
2530
|
|
|
@@ -2617,6 +2622,14 @@ Read the sealed credentials of a plugin-declared connection (auto- refreshes exp
|
|
|
2617
2622
|
function getPluginConnectionCredentials( _connectionId: string, _pluginId: string, ): Promise<PluginConnectionCredentials>
|
|
2618
2623
|
```
|
|
2619
2624
|
|
|
2625
|
+
#### `headlessBridge` — function · src/server.ts
|
|
2626
|
+
|
|
2627
|
+
Run a web page in the platform's headless browser and drive it over `postMessage` — see the `HeadlessBridgeArgs` note above: `url` (https, public), `boot` (the page's ready message), `calls` (each posted to the page's window, answered by the first message whose `[replyField]` equals its `id`). Warm between calls for the same url; not available in a Forge box.
|
|
2628
|
+
|
|
2629
|
+
```ts
|
|
2630
|
+
function headlessBridge(_ctx: { pluginId: string; nodeId: string; origin?: string }, _args: HeadlessBridgeArgs): Promise<HeadlessBridgeResult>
|
|
2631
|
+
```
|
|
2632
|
+
|
|
2620
2633
|
#### `isCredentialUnavailable` — function · src/server.ts
|
|
2621
2634
|
|
|
2622
2635
|
True for a `CredentialUnavailable`, also across bundle boundaries where `instanceof` fails.
|
|
@@ -2809,7 +2822,7 @@ WHO IS THIS, IN WORDS. The caller's own name and email, for an app with a reason
|
|
|
2809
2822
|
function viewerProfile(_viewer: PluginViewer): Promise<ViewerProfile | null>
|
|
2810
2823
|
```
|
|
2811
2824
|
|
|
2812
|
-
### Types (
|
|
2825
|
+
### Types (82)
|
|
2813
2826
|
|
|
2814
2827
|
#### `AppQuestion` — interface · src/server.ts
|
|
2815
2828
|
|
|
@@ -3304,6 +3317,41 @@ interface GeneratedAppImage {
|
|
|
3304
3317
|
}
|
|
3305
3318
|
```
|
|
3306
3319
|
|
|
3320
|
+
#### `HeadlessBridgeArgs` — interface · src/server.ts
|
|
3321
|
+
|
|
3322
|
+
```ts
|
|
3323
|
+
interface HeadlessBridgeArgs {
|
|
3324
|
+
url: string;
|
|
3325
|
+
boot?: { field: string; oneOf: string[]; timeoutMs?: number };
|
|
3326
|
+
calls: HeadlessBridgeCall[];
|
|
3327
|
+
viewport?: { width: number; height: number };
|
|
3328
|
+
}
|
|
3329
|
+
```
|
|
3330
|
+
|
|
3331
|
+
#### `HeadlessBridgeCall` — interface · src/server.ts
|
|
3332
|
+
|
|
3333
|
+
The headless bridge — a page opened in a headless browser ON THE PLATFORM and driven over `postMessage`, for an app whose engine only exists as a web page (a WebAssembly CAD kernel, a canvas renderer). "No computer should be required": the person's tab is one place the page can run; this is the other, on Vercel, with nobody's tab open.
|
|
3334
|
+
|
|
3335
|
+
```ts
|
|
3336
|
+
interface HeadlessBridgeCall {
|
|
3337
|
+
id: string;
|
|
3338
|
+
message: Record<string, unknown>;
|
|
3339
|
+
replyField?: string;
|
|
3340
|
+
timeoutMs?: number;
|
|
3341
|
+
}
|
|
3342
|
+
```
|
|
3343
|
+
|
|
3344
|
+
#### `HeadlessBridgeResult` — interface · src/server.ts
|
|
3345
|
+
|
|
3346
|
+
```ts
|
|
3347
|
+
interface HeadlessBridgeResult {
|
|
3348
|
+
boot: Record<string, unknown> | null;
|
|
3349
|
+
replies: (Record<string, unknown> | null)[];
|
|
3350
|
+
timings: Record<string, number>;
|
|
3351
|
+
warm: boolean;
|
|
3352
|
+
}
|
|
3353
|
+
```
|
|
3354
|
+
|
|
3307
3355
|
#### `JsonOutcome` — interface · src/computer.ts
|
|
3308
3356
|
|
|
3309
3357
|
Flat, like every outcome here. `ok` with `json`; otherwise `error` in words.
|
package/dist/server.d.ts
CHANGED
|
@@ -681,6 +681,56 @@ export declare function mintRouteToken(_ctx: {
|
|
|
681
681
|
ttlSeconds?: number;
|
|
682
682
|
label?: string;
|
|
683
683
|
}): Promise<RouteTokenGrant>;
|
|
684
|
+
/**
|
|
685
|
+
* The headless bridge — a page opened in a headless browser ON THE PLATFORM and driven over
|
|
686
|
+
* `postMessage`, for an app whose engine only exists as a web page (a WebAssembly CAD kernel, a
|
|
687
|
+
* canvas renderer). "No computer should be required": the person's tab is one place the page
|
|
688
|
+
* can run; this is the other, on Vercel, with nobody's tab open.
|
|
689
|
+
*
|
|
690
|
+
* `url` must be https and public. The page is loaded top-level, so it is its own `parent`: a
|
|
691
|
+
* bridge written for an iframe (`?parent=<origin>` + `window.parent.postMessage`) answers
|
|
692
|
+
* unchanged when the url names the page's own origin as the parent. `boot` waits for the page's
|
|
693
|
+
* ready message; each call posts `message` to the page's window and returns the first message
|
|
694
|
+
* whose `[replyField]` (default "id") equals the call's `id`. The page stays warm on the platform
|
|
695
|
+
* between calls for the same url — the first call pays the launch, later ones only the calls.
|
|
696
|
+
* Not available in a Forge preview (the box has no browser): keep the app open there instead.
|
|
697
|
+
*/
|
|
698
|
+
export interface HeadlessBridgeCall {
|
|
699
|
+
id: string;
|
|
700
|
+
message: Record<string, unknown>;
|
|
701
|
+
replyField?: string;
|
|
702
|
+
timeoutMs?: number;
|
|
703
|
+
}
|
|
704
|
+
export interface HeadlessBridgeArgs {
|
|
705
|
+
url: string;
|
|
706
|
+
boot?: {
|
|
707
|
+
field: string;
|
|
708
|
+
oneOf: string[];
|
|
709
|
+
timeoutMs?: number;
|
|
710
|
+
};
|
|
711
|
+
calls: HeadlessBridgeCall[];
|
|
712
|
+
viewport?: {
|
|
713
|
+
width: number;
|
|
714
|
+
height: number;
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
export interface HeadlessBridgeResult {
|
|
718
|
+
boot: Record<string, unknown> | null;
|
|
719
|
+
replies: (Record<string, unknown> | null)[];
|
|
720
|
+
timings: Record<string, number>;
|
|
721
|
+
warm: boolean;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Run a web page in the platform's headless browser and drive it over `postMessage` — see the
|
|
725
|
+
* `HeadlessBridgeArgs` note above: `url` (https, public), `boot` (the page's ready message),
|
|
726
|
+
* `calls` (each posted to the page's window, answered by the first message whose `[replyField]`
|
|
727
|
+
* equals its `id`). Warm between calls for the same url; not available in a Forge box.
|
|
728
|
+
*/
|
|
729
|
+
export declare function headlessBridge(_ctx: {
|
|
730
|
+
pluginId: string;
|
|
731
|
+
nodeId: string;
|
|
732
|
+
origin?: string;
|
|
733
|
+
}, _args: HeadlessBridgeArgs): Promise<HeadlessBridgeResult>;
|
|
684
734
|
/** A question for the person (questions(ctx).ask). */
|
|
685
735
|
export interface AppQuestion {
|
|
686
736
|
/** Stable per question within this app instance: asking twice with one key is one question. */
|
package/dist/server.js
CHANGED
|
@@ -266,6 +266,15 @@ export function renderChartImage(_ctx, _args) {
|
|
|
266
266
|
export function mintRouteToken(_ctx, _args) {
|
|
267
267
|
return hostOnly("mintRouteToken");
|
|
268
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Run a web page in the platform's headless browser and drive it over `postMessage` — see the
|
|
271
|
+
* `HeadlessBridgeArgs` note above: `url` (https, public), `boot` (the page's ready message),
|
|
272
|
+
* `calls` (each posted to the page's window, answered by the first message whose `[replyField]`
|
|
273
|
+
* equals its `id`). Warm between calls for the same url; not available in a Forge box.
|
|
274
|
+
*/
|
|
275
|
+
export function headlessBridge(_ctx, _args) {
|
|
276
|
+
return hostOnly("headlessBridge");
|
|
277
|
+
}
|
|
269
278
|
/**
|
|
270
279
|
* THE PLATFORM'S QUESTIONS BELL. A question an app needs the person to answer
|
|
271
280
|
* (an agent waiting on a decision) reaches them wherever they are — the bell,
|
package/dist/types.d.ts
CHANGED
|
@@ -140,6 +140,21 @@ export interface EventDefinition<StateType extends ApplicationIdentifier> {
|
|
|
140
140
|
* back to the normal conflict record. Pure. See docs/17-editing-and-merging.md.
|
|
141
141
|
*/
|
|
142
142
|
baseMerge?: BaseMerge;
|
|
143
|
+
/**
|
|
144
|
+
* MERGE A CONCURRENT WRITE BEFORE IT BECOMES A CONFLICT. Called by the fence only when this event would be
|
|
145
|
+
* applied as a conflict — the sub-items it writes (`merge.scope`) were changed by a writer it never saw. The
|
|
146
|
+
* event knows its data: it merges what it can against the base it carries and the state it lands on, and
|
|
147
|
+
* returns the rewritten event. `detail` undefined → everything merged, no conflict marker; `detail` set → what
|
|
148
|
+
* could not be merged, stored on the marker for the resolution UI (the app's own shape); `null` → cannot reason
|
|
149
|
+
* about it (the plain conflict stands: applied and marked). Pure and deterministic — it runs in every fold.
|
|
150
|
+
* See docs/17-editing-and-merging.md §4.
|
|
151
|
+
*/
|
|
152
|
+
resolveConcurrent?: (state: any, eventData: any, ctx: {
|
|
153
|
+
theirsSess: string | null;
|
|
154
|
+
}) => {
|
|
155
|
+
eventData: any;
|
|
156
|
+
detail?: unknown;
|
|
157
|
+
} | null;
|
|
143
158
|
}
|
|
144
159
|
export interface BaseMerge<V = any> {
|
|
145
160
|
/** The value this event writes, as the state holds it right now (theirs). `undefined` → nothing to merge. */
|
package/docs/06-server.md
CHANGED
|
@@ -313,3 +313,31 @@ your own nodeId) for your timeline; a route context has no `emit` of its own.
|
|
|
313
313
|
the example stops streaming — nothing durable happened unless the handler wrote events. A
|
|
314
314
|
task (docs/07) survives everything and costs seconds per hop. The stopwatch ships both and
|
|
315
315
|
records which one drove each run; measure before you choose.
|
|
316
|
+
|
|
317
|
+
## The headless bridge — a browser on the platform, for a page-shaped engine
|
|
318
|
+
|
|
319
|
+
Some engines exist only as a web page: a WebAssembly CAD kernel, a canvas renderer. The
|
|
320
|
+
person's open tab can run it; `headlessBridge(ctx, …)` runs it on the platform too, so an agent
|
|
321
|
+
with nobody's tab open (a phone with the app closed, an agent network, MCP) gets the same work
|
|
322
|
+
done — no computer of the person's involved.
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import { headlessBridge } from "esoul-sdk/server";
|
|
326
|
+
|
|
327
|
+
const r = await headlessBridge(ctx, {
|
|
328
|
+
url: "https://my-runtime.example/page.html?parent=https%3A%2F%2Fmy-runtime.example",
|
|
329
|
+
boot: { field: "type", oneOf: ["ready", "error"] }, // the page's own ready message
|
|
330
|
+
calls: [{ id: "replay", message: { myBridge: 1, id: "replay", method: "replay", args: { … } } }],
|
|
331
|
+
});
|
|
332
|
+
r.boot; // the ready message
|
|
333
|
+
r.replies[0]; // the first message whose `id` equals "replay"
|
|
334
|
+
r.timings; // launchMs / bootMs (cold only), per-call ms, totalMs
|
|
335
|
+
r.warm; // true when the page was already open on the platform
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
The page is loaded top-level, so it is its own `parent`: a bridge written for an iframe answers
|
|
339
|
+
unchanged when `?parent=` names the page's own origin. The browser and the loaded page stay warm
|
|
340
|
+
on the platform between calls for the same url; the first call pays the launch and the page's
|
|
341
|
+
download, later calls only the calls. `url` must be https and public; at most 32 calls; replies
|
|
342
|
+
up to 24 MB. A Forge box has no browser: there the call throws and says so — keep the app open
|
|
343
|
+
in the preview and let the tab do the work.
|
|
@@ -153,3 +153,25 @@ recorded as a conflict. `baseMerge` runs in every fold: keep it pure, exactly li
|
|
|
153
153
|
platform and needs no merge — prefer operations to whole-value saves where you can.
|
|
154
154
|
- Device view state (which page is open, scroll, selection) belongs in component state or
|
|
155
155
|
`sessionStorage`, never in a shared event another device would follow.
|
|
156
|
+
|
|
157
|
+
## 4. Declare `resolveConcurrent` for patch-shaped events
|
|
158
|
+
|
|
159
|
+
`baseMerge` is for events that write one whole value. An event that writes a PATCH — a list of sub-items it
|
|
160
|
+
changed, each with the version it built on — can do better: declare `merge` with `scope` (the sub-item ids),
|
|
161
|
+
and `resolveConcurrent`. The platform calls it only when the patch would land as a conflict (a writer it never
|
|
162
|
+
saw changed one of its sub-items), with the state it lands on; it merges what it can against the bases it
|
|
163
|
+
carries and returns the rewritten event:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
merge: { item: (_d, ctx) => `${ctx.applicationId}:cad:model`, scope: (d) => d.delta.ops.map(keyOf), describe: (d) => d.label },
|
|
167
|
+
resolveConcurrent: (state, eventData, { theirsSess }) => {
|
|
168
|
+
const r = resolveEdit(state, eventData); // pure: three-way per sub-item against the base each op carries
|
|
169
|
+
if (!r) return null; // cannot reason about it → the plain conflict (applied and marked)
|
|
170
|
+
return r.clashes.length === 0
|
|
171
|
+
? { eventData: r.eventData } // everything merged: applied, no marker
|
|
172
|
+
: { eventData: r.eventData, detail: { kind: "cad-edit", clashes: r.clashes, theirsSess } };
|
|
173
|
+
},
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
What it returns is what the fold applies, on every device and on the server. Keep it pure and symmetric; a
|
|
177
|
+
property test over randomised edit pairs is the cheapest proof.
|
package/llms-full.txt
CHANGED
|
@@ -1669,6 +1669,34 @@ the example stops streaming — nothing durable happened unless the handler wrot
|
|
|
1669
1669
|
task (docs/07) survives everything and costs seconds per hop. The stopwatch ships both and
|
|
1670
1670
|
records which one drove each run; measure before you choose.
|
|
1671
1671
|
|
|
1672
|
+
## The headless bridge — a browser on the platform, for a page-shaped engine
|
|
1673
|
+
|
|
1674
|
+
Some engines exist only as a web page: a WebAssembly CAD kernel, a canvas renderer. The
|
|
1675
|
+
person's open tab can run it; `headlessBridge(ctx, …)` runs it on the platform too, so an agent
|
|
1676
|
+
with nobody's tab open (a phone with the app closed, an agent network, MCP) gets the same work
|
|
1677
|
+
done — no computer of the person's involved.
|
|
1678
|
+
|
|
1679
|
+
```ts
|
|
1680
|
+
import { headlessBridge } from "esoul-sdk/server";
|
|
1681
|
+
|
|
1682
|
+
const r = await headlessBridge(ctx, {
|
|
1683
|
+
url: "https://my-runtime.example/page.html?parent=https%3A%2F%2Fmy-runtime.example",
|
|
1684
|
+
boot: { field: "type", oneOf: ["ready", "error"] }, // the page's own ready message
|
|
1685
|
+
calls: [{ id: "replay", message: { myBridge: 1, id: "replay", method: "replay", args: { … } } }],
|
|
1686
|
+
});
|
|
1687
|
+
r.boot; // the ready message
|
|
1688
|
+
r.replies[0]; // the first message whose `id` equals "replay"
|
|
1689
|
+
r.timings; // launchMs / bootMs (cold only), per-call ms, totalMs
|
|
1690
|
+
r.warm; // true when the page was already open on the platform
|
|
1691
|
+
```
|
|
1692
|
+
|
|
1693
|
+
The page is loaded top-level, so it is its own `parent`: a bridge written for an iframe answers
|
|
1694
|
+
unchanged when `?parent=` names the page's own origin. The browser and the loaded page stay warm
|
|
1695
|
+
on the platform between calls for the same url; the first call pays the launch and the page's
|
|
1696
|
+
download, later calls only the calls. `url` must be https and public; at most 32 calls; replies
|
|
1697
|
+
up to 24 MB. A Forge box has no browser: there the call throws and says so — keep the app open
|
|
1698
|
+
in the preview and let the tab do the work.
|
|
1699
|
+
|
|
1672
1700
|
|
|
1673
1701
|
|
|
1674
1702
|
==============================================================================
|
|
@@ -3598,6 +3626,28 @@ recorded as a conflict. `baseMerge` runs in every fold: keep it pure, exactly li
|
|
|
3598
3626
|
- Device view state (which page is open, scroll, selection) belongs in component state or
|
|
3599
3627
|
`sessionStorage`, never in a shared event another device would follow.
|
|
3600
3628
|
|
|
3629
|
+
## 4. Declare `resolveConcurrent` for patch-shaped events
|
|
3630
|
+
|
|
3631
|
+
`baseMerge` is for events that write one whole value. An event that writes a PATCH — a list of sub-items it
|
|
3632
|
+
changed, each with the version it built on — can do better: declare `merge` with `scope` (the sub-item ids),
|
|
3633
|
+
and `resolveConcurrent`. The platform calls it only when the patch would land as a conflict (a writer it never
|
|
3634
|
+
saw changed one of its sub-items), with the state it lands on; it merges what it can against the bases it
|
|
3635
|
+
carries and returns the rewritten event:
|
|
3636
|
+
|
|
3637
|
+
```ts
|
|
3638
|
+
merge: { item: (_d, ctx) => `${ctx.applicationId}:cad:model`, scope: (d) => d.delta.ops.map(keyOf), describe: (d) => d.label },
|
|
3639
|
+
resolveConcurrent: (state, eventData, { theirsSess }) => {
|
|
3640
|
+
const r = resolveEdit(state, eventData); // pure: three-way per sub-item against the base each op carries
|
|
3641
|
+
if (!r) return null; // cannot reason about it → the plain conflict (applied and marked)
|
|
3642
|
+
return r.clashes.length === 0
|
|
3643
|
+
? { eventData: r.eventData } // everything merged: applied, no marker
|
|
3644
|
+
: { eventData: r.eventData, detail: { kind: "cad-edit", clashes: r.clashes, theirsSess } };
|
|
3645
|
+
},
|
|
3646
|
+
```
|
|
3647
|
+
|
|
3648
|
+
What it returns is what the fold applies, on every device and on the server. Keep it pure and symmetric; a
|
|
3649
|
+
property test over randomised edit pairs is the cheapest proof.
|
|
3650
|
+
|
|
3601
3651
|
|
|
3602
3652
|
|
|
3603
3653
|
==============================================================================
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "esoul-sdk",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.2",
|
|
4
4
|
"description": "Build a full product on ExternalSoul: your own tables with per-person rules, a viewer on every seam, app roles, access levels, realtime with audiences, durable tasks, and bindings to other apps.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|