Initial scaffold: @ng-eventually/client — SDK-shaped polyfill over NextGraph
Generic polyfill layer that makes a single NextGraph broker behave like the not-yet-shipped multi-user NextGraph (emulated capabilities + inbox). Zero app domain. @ng-eventually/client exposes an SDK-identical surface (ng, useShape, inbox); the polyfill bootstrap (configure + capability helpers) is isolated under /polyfill, so the main entry stays a drop-in for @ng-org/web|orm. The real SDK is injected at configure() (no hard import → build-alias safe + testable). Scaffold: NextGraph wiring stubbed with TODO; capability helpers implemented and unit-tested (4 tests, typecheck clean). The global-index curator is deferred — in NextGraph apps/services are mono-user with no global data. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.tsbuildinfo
|
||||||
|
.DS_Store
|
||||||
|
bun.lockb
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# ng-eventually
|
||||||
|
|
||||||
|
A **generic polyfill layer** over the [NextGraph](https://nextgraph.org) JS SDK.
|
||||||
|
|
||||||
|
NextGraph's JS SDK does not yet expose cross-wallet reads, capabilities, inboxes
|
||||||
|
or group stores. `ng-eventually` lets an app behave as if those existed today, by
|
||||||
|
emulating them on top of a **single shared wallet / broker**. It is **generic**:
|
||||||
|
it contains **no application domain** — the consumer injects its shapes and the
|
||||||
|
*acts* of granting access.
|
||||||
|
|
||||||
|
The name: *eventually* NextGraph will ship these features; until then this layer
|
||||||
|
fills the gap (and nods at eventual consistency / events).
|
||||||
|
|
||||||
|
## Packages
|
||||||
|
|
||||||
|
| Package | Role | At migration |
|
||||||
|
|---|---|---|
|
||||||
|
| **`@ng-eventually/client`** | **SDK-identical** wrapper the app imports instead of `@ng-org/web` / `@ng-org/orm`. Adds the polyfills the broker/verifier will do natively (shared-wallet login, capability enforcement, anticipated cap/inbox methods). | Disappears: the app points back at the real SDK (build alias removed). |
|
||||||
|
|
||||||
|
> **A global-index curator package is deferred.** NextGraph is **mono-user with
|
||||||
|
> no global data** (apps/services see only what the user shares; there is no
|
||||||
|
> multi-user backend). A global index would come from a **singleton app**
|
||||||
|
> (a global document administered by the developer) — **not implemented and
|
||||||
|
> uncertain**, and simpler paths may exist. So no second package for now; it will
|
||||||
|
> be (re)introduced once the global-index mechanism is decided. The curator must
|
||||||
|
> never be bundled in the client → it will be a separate package when it lands.
|
||||||
|
|
||||||
|
## Design principle
|
||||||
|
|
||||||
|
The application code is written **as if the target NextGraph existed**. All
|
||||||
|
compensation lives here, *beside* the app. Migration = remove this layer; the
|
||||||
|
app code (SDK-shaped) is unchanged.
|
||||||
|
|
||||||
|
- **SDK-identical surface**: the client wraps the real `ng` (a Proxy that
|
||||||
|
forwards everything and overrides only what must be emulated) and `useShape`.
|
||||||
|
The real SDK is **injected** via `configure()` (no hard import → build-alias
|
||||||
|
safe + testable).
|
||||||
|
- **Authorization = emulated capabilities**: documents carry grants; the client
|
||||||
|
enforces them generically (read filter + write guard). The app *attaches*
|
||||||
|
grants via cap operations — same as it will in the target. No policy is
|
||||||
|
injected.
|
||||||
|
- **Inbox**: the client `inbox.post()` deposits; materialization (the curator)
|
||||||
|
is deferred with the global-index mechanism — see the note above.
|
||||||
|
- **Tests** of the polyfill (against a real broker) live **in this repo**, so
|
||||||
|
the consuming app can test its features against a mocked, clean API.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Scaffold. Mechanisms are stubbed with `TODO(broker)` / `TODO(polyfill)` where
|
||||||
|
real NextGraph wiring is required.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "ng-eventually",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"typescript": "^5.6.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages/client": {
|
||||||
|
"name": "@ng-eventually/client",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@ng-org/orm": "*",
|
||||||
|
"@ng-org/web": "*",
|
||||||
|
},
|
||||||
|
"optionalPeers": [
|
||||||
|
"@ng-org/orm",
|
||||||
|
"@ng-org/web",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@ng-eventually/client": ["@ng-eventually/client@workspace:packages/client"],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "ng-eventually",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"description": "Generic polyfill layer over the NextGraph JS SDK — makes a single broker behave like the (not-yet-shipped) multi-user NextGraph: emulated capabilities and inbox. Drop-in SDK-shaped client; remove at migration. (A global-index curator package is deferred — see README.)",
|
||||||
|
"workspaces": ["packages/*"],
|
||||||
|
"scripts": {
|
||||||
|
"test": "bun test",
|
||||||
|
"typecheck": "bunx tsc --noEmit -p packages/client/tsconfig.json"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"typescript": "^5.6.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# @ng-eventually/client
|
||||||
|
|
||||||
|
Two entry points — the data-plane is **SDK-identical**, the polyfill bootstrap is
|
||||||
|
separate:
|
||||||
|
|
||||||
|
| Import | Surface | At migration |
|
||||||
|
|---|---|---|
|
||||||
|
| `@ng-eventually/client` | **Same signature as the SDK** — `ng`, `useShape`, `inbox` (+ types). Drop-in for `@ng-org/web` / `@ng-org/orm`. | Resolves to the real SDK (build alias removed) — no code change. |
|
||||||
|
| `@ng-eventually/client/polyfill` | The **only non-SDK** surface — `configure`, `setCurrentUser`, capability helpers (`canRead`/`canWrite`/`defaultGrant`/`grantRead`). | Removed. |
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// bootstrap (the only non-SDK call) — inject the REAL SDK
|
||||||
|
import { configure } from "@ng-eventually/client/polyfill";
|
||||||
|
configure({ ng: realNg, useShape: realUseShape, sharedWallet, currentUser });
|
||||||
|
|
||||||
|
// from here on, pure SDK surface:
|
||||||
|
import { ng, useShape, inbox } from "@ng-eventually/client";
|
||||||
|
await ng.doc_create(/* … */);
|
||||||
|
const set = useShape(MyShape, scope); // filtered to what the user may read
|
||||||
|
await inbox.post(targetInbox, ref); // deposit (anticipated SDK API)
|
||||||
|
```
|
||||||
|
|
||||||
|
What the polyfill adds on top of the real SDK (all removed/native at migration):
|
||||||
|
- **Shared-wallet login** (one wallet for everyone).
|
||||||
|
- **Capability enforcement** — read filter + write guard, on emulated grants
|
||||||
|
attached to documents.
|
||||||
|
- **Anticipated methods** (inbox `post`, capability ops) with their future-SDK
|
||||||
|
shapes, emulated for now.
|
||||||
|
|
||||||
|
Generic: **no application domain**. The consumer injects shapes and performs the
|
||||||
|
*acts* of granting access. The client **must not** contain the global-index
|
||||||
|
curator (a separate package, deferred — see the repo README).
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "@ng-eventually/client",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"description": "SDK-identical client wrapper over @ng-org/web + @ng-org/orm with emulated capabilities and inbox. Drop-in; remove at migration.",
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts",
|
||||||
|
"./polyfill": "./src/polyfill.ts"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@ng-org/web": "*",
|
||||||
|
"@ng-org/orm": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@ng-org/web": { "optional": true },
|
||||||
|
"@ng-org/orm": { "optional": true }
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "bun test"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Capability emulation — pure, generic, no domain rules. Mirrors what the broker
|
||||||
|
* will enforce via real caps; at migration these checks disappear (the broker
|
||||||
|
* only ever syncs/serves what the wallet is authorized for).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Grant, PrincipalId, Scope } from "./types";
|
||||||
|
|
||||||
|
/** May `principal` read a document carrying `grant`? */
|
||||||
|
export function canRead(grant: Grant, principal: PrincipalId | null): boolean {
|
||||||
|
if (grant.read.includes("public")) return true;
|
||||||
|
if (principal === null) return false;
|
||||||
|
return grant.read.includes(principal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** May `principal` write a document carrying `grant`? */
|
||||||
|
export function canWrite(grant: Grant, principal: PrincipalId | null): boolean {
|
||||||
|
if (principal === null) return false;
|
||||||
|
return grant.write.includes(principal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The grant a freshly created document gets, by scope, for its `owner`. These
|
||||||
|
* are the *defaults* a creator attaches; further sharing (e.g. granting a
|
||||||
|
* connection read on a protected doc) is a separate explicit act.
|
||||||
|
*/
|
||||||
|
export function defaultGrant(scope: Scope, owner: PrincipalId): Grant {
|
||||||
|
switch (scope) {
|
||||||
|
case "public":
|
||||||
|
return { read: ["public"], write: [owner] };
|
||||||
|
case "protected":
|
||||||
|
return { read: [owner], write: [owner] };
|
||||||
|
case "private":
|
||||||
|
return { read: [owner], write: [owner] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add a read grant to a principal (e.g. when a connection is accepted). */
|
||||||
|
export function grantRead(grant: Grant, principal: PrincipalId): Grant {
|
||||||
|
if (grant.read.includes(principal)) return grant;
|
||||||
|
return { ...grant, read: [...grant.read, principal] };
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Inbox — client side (deposit only). The MATERIALIZATION of deposits is the
|
||||||
|
* curator's job (a separate package, deferred — see repo README), never the client's.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getConfig, getCurrentUser } from "./polyfill";
|
||||||
|
import type { Nuri } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deposit a reference into a document's inbox (anticipated SDK shape). Target:
|
||||||
|
* `ng.inbox_post_link(...)`, sealed to the inbox owner; `from` optional → the
|
||||||
|
* sender is identified if known, anonymous otherwise. Polyfill: append a
|
||||||
|
* deposit to the emulated inbox document.
|
||||||
|
*/
|
||||||
|
export async function post(
|
||||||
|
targetInbox: Nuri,
|
||||||
|
payload: unknown,
|
||||||
|
opts?: { anonymous?: boolean },
|
||||||
|
): Promise<void> {
|
||||||
|
const { ng } = getConfig();
|
||||||
|
const from = opts?.anonymous ? null : getCurrentUser();
|
||||||
|
// TODO(polyfill): append { from, payload } to the emulated inbox document
|
||||||
|
// (targetInbox) via ng.sparql_update.
|
||||||
|
void ng;
|
||||||
|
void from;
|
||||||
|
void targetInbox;
|
||||||
|
void payload;
|
||||||
|
throw new Error("[ng-eventually] inbox.post: polyfill emulation not yet implemented");
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* @ng-eventually/client — **SDK-identical** surface.
|
||||||
|
*
|
||||||
|
* This entry exposes ONLY what `@ng-org/web` / `@ng-org/orm` expose (current +
|
||||||
|
* anticipated: `inbox`). Import `ng` / `useShape` from here instead of the SDK
|
||||||
|
* during the polyfill period; at migration the build alias is removed and these
|
||||||
|
* resolve to the real SDK with **no code change**.
|
||||||
|
*
|
||||||
|
* The one non-SDK piece — the polyfill bootstrap (`configure`, capability
|
||||||
|
* helpers, current user) — lives at `@ng-eventually/client/polyfill`, and is the
|
||||||
|
* only thing removed at migration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./types";
|
||||||
|
export { useShape } from "./use-shape";
|
||||||
|
export * as inbox from "./inbox";
|
||||||
|
|
||||||
|
import { makeNg } from "./ng-proxy";
|
||||||
|
|
||||||
|
/** SDK-identical `ng` (wrapped). Drop-in replacement for `@ng-org/web`'s `ng`. */
|
||||||
|
export const ng: Record<string, any> = makeNg();
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* The wrapped `ng`: a Proxy that forwards every method to the real SDK and
|
||||||
|
* overrides only what the broker/verifier will do natively at migration. The
|
||||||
|
* surface stays identical to `@ng-org/web`'s `ng`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getConfig } from "./polyfill";
|
||||||
|
|
||||||
|
export function makeNg(): Record<string, any> {
|
||||||
|
return new Proxy({} as Record<string, any>, {
|
||||||
|
get(_target, prop: string) {
|
||||||
|
const { ng } = getConfig();
|
||||||
|
|
||||||
|
// login / session_start → open the SHARED wallet invisibly.
|
||||||
|
if (prop === "login" || prop === "session_start") {
|
||||||
|
return (...args: any[]) => {
|
||||||
|
// TODO(polyfill): supply shared-wallet credentials so no wallet UI
|
||||||
|
// is shown. For now, passthrough.
|
||||||
|
return ng[prop]!(...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// sparql_update → write guard (emulated write-cap check).
|
||||||
|
if (prop === "sparql_update") {
|
||||||
|
return (...args: any[]) => {
|
||||||
|
// TODO(polyfill): reject when getCurrentUser() lacks the write grant
|
||||||
|
// on the target document (see access.canWrite). For now, passthrough.
|
||||||
|
return ng.sparql_update!(...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(anticipated API): inbox_post_link + capability operations — expose
|
||||||
|
// here with their anticipated signatures, emulated for now.
|
||||||
|
|
||||||
|
// Everything else: passthrough to the real SDK, unchanged.
|
||||||
|
const real = ng[prop];
|
||||||
|
return typeof real === "function" ? real.bind(ng) : real;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* The polyfill bootstrap — the ONLY non-SDK surface of the client.
|
||||||
|
*
|
||||||
|
* It injects the REAL SDK and the polyfill settings; afterwards the SDK-shaped
|
||||||
|
* exports (`ng`, `useShape`, `inbox`) behave as drop-ins. This is exposed at the
|
||||||
|
* subpath `@ng-eventually/client/polyfill` so the main entry
|
||||||
|
* (`@ng-eventually/client`) stays a **pure, SDK-identical** surface. Everything
|
||||||
|
* here is removed at migration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { NgLike, UseShapeLike, PrincipalId } from "./types";
|
||||||
|
|
||||||
|
export interface EventuallyConfig {
|
||||||
|
/** The REAL `@ng-org/web` `ng` (injected to avoid a hard import / alias loop). */
|
||||||
|
ng: NgLike;
|
||||||
|
/** The REAL `@ng-org/orm` `useShape`. */
|
||||||
|
useShape: UseShapeLike;
|
||||||
|
/** Shared-wallet credentials — polyfill only (one wallet for everyone). */
|
||||||
|
sharedWallet?: { name: string; secret: string };
|
||||||
|
/** Initial current user; may also be set later via {@link setCurrentUser}. */
|
||||||
|
currentUser?: PrincipalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cfg: EventuallyConfig | null = null;
|
||||||
|
let currentUser: PrincipalId | null = null;
|
||||||
|
|
||||||
|
export function configure(c: EventuallyConfig): void {
|
||||||
|
cfg = c;
|
||||||
|
currentUser = c.currentUser ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal — used by the SDK-shaped wrappers to reach the injected real SDK. */
|
||||||
|
export function getConfig(): EventuallyConfig {
|
||||||
|
if (!cfg) throw new Error("[ng-eventually] configure() must be called before use");
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set the current app-level user (the polyfill's notion of "who am I"). */
|
||||||
|
export function setCurrentUser(id: PrincipalId | null): void {
|
||||||
|
currentUser = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentUser(): PrincipalId | null {
|
||||||
|
return currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capability helpers — polyfill-era surface (caps are emulated now; native at
|
||||||
|
// migration). Re-exported here so the whole polyfill API lives under /polyfill.
|
||||||
|
export { canRead, canWrite, defaultGrant, grantRead } from "./access";
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Generic, NextGraph-shaped types. ZERO application domain.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** A NextGraph URI (document / store / inbox). */
|
||||||
|
export type Nuri = string;
|
||||||
|
|
||||||
|
/** NextGraph-native store scopes. The *mapping* of entities to scopes is the
|
||||||
|
* consumer's concern; this layer only knows the three scopes exist. */
|
||||||
|
export type Scope = "public" | "protected" | "private";
|
||||||
|
|
||||||
|
/** App-level current identity. Target: the wallet user (`session.user`).
|
||||||
|
* Polyfill: a chosen id (e.g. a username), because everyone shares one wallet. */
|
||||||
|
export type PrincipalId = string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emulated capability attached to a document — mirrors real NextGraph caps,
|
||||||
|
* which the broker/verifier will enforce natively at migration. The app attaches
|
||||||
|
* these via cap operations exactly as it will in the target; this layer enforces
|
||||||
|
* them generically (it does NOT know any authorization policy).
|
||||||
|
*/
|
||||||
|
export interface Grant {
|
||||||
|
/** Read grant: `"public"` = everyone, otherwise explicit principals. */
|
||||||
|
read: Array<PrincipalId | "public">;
|
||||||
|
/** Write grant: principals allowed to write the document. */
|
||||||
|
write: PrincipalId[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loose shape of the real `@ng-org/web` `ng` object that we wrap. Injected by
|
||||||
|
* the consumer at {@link configure} — we never hard-import the SDK, which keeps
|
||||||
|
* the build-alias safe (the app's `@ng-org/web` import can resolve to us) and
|
||||||
|
* makes the wrapper testable with a fake. Permissive on purpose: the real `ng`
|
||||||
|
* carries non-function members too, so we accept any property bag.
|
||||||
|
*/
|
||||||
|
export type NgLike = Record<string, any>;
|
||||||
|
|
||||||
|
/** Loose shape of `@ng-org/orm`'s `useShape` (a generic hook). */
|
||||||
|
export type UseShapeLike = (...args: any[]) => any;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Wrapped `useShape`: same signature as `@ng-org/orm`. The returned reactive set
|
||||||
|
* is filtered to what the current user may read (emulated caps). At migration
|
||||||
|
* this filtering disappears — the broker only syncs authorized documents.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getConfig } from "./polyfill";
|
||||||
|
|
||||||
|
export function useShape(shapeType: unknown, scope: unknown): unknown {
|
||||||
|
const { useShape: real } = getConfig();
|
||||||
|
const set = real(shapeType, scope);
|
||||||
|
// TODO(polyfill): wrap `set` so iteration yields only entries whose emulated
|
||||||
|
// grant satisfies access.canRead(grant, getCurrentUser()), while preserving
|
||||||
|
// the reactivity of the underlying DeepSignalSet. This is the trickiest piece.
|
||||||
|
return set;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { test, expect } from "bun:test";
|
||||||
|
import { canRead, canWrite, defaultGrant, grantRead } from "../src/access";
|
||||||
|
|
||||||
|
test("public docs are readable by anyone, even anonymous", () => {
|
||||||
|
const g = defaultGrant("public", "alice");
|
||||||
|
expect(canRead(g, null)).toBe(true);
|
||||||
|
expect(canRead(g, "bob")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("protected docs: owner + explicitly granted principals only", () => {
|
||||||
|
let g = defaultGrant("protected", "alice");
|
||||||
|
expect(canRead(g, "alice")).toBe(true);
|
||||||
|
expect(canRead(g, "bob")).toBe(false);
|
||||||
|
g = grantRead(g, "bob"); // e.g. bob becomes a connection of alice
|
||||||
|
expect(canRead(g, "bob")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("private docs: owner only", () => {
|
||||||
|
const g = defaultGrant("private", "alice");
|
||||||
|
expect(canRead(g, "alice")).toBe(true);
|
||||||
|
expect(canRead(g, "bob")).toBe(false);
|
||||||
|
expect(canRead(g, null)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("write is restricted to the write grant", () => {
|
||||||
|
const g = defaultGrant("public", "alice");
|
||||||
|
expect(canWrite(g, "alice")).toBe(true);
|
||||||
|
expect(canWrite(g, "bob")).toBe(false);
|
||||||
|
expect(canWrite(g, null)).toBe(false);
|
||||||
|
});
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"include": ["src", "test"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"declaration": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user