rastack 0.0.51 → 0.0.52

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/auth/context.ts +73 -0
  3. package/auth/core.ts +293 -0
  4. package/auth/index.ts +23 -0
  5. package/components/auto-form/AutoForm.tsx +32 -12
  6. package/components/auto-form/use-auto-form.ts +75 -6
  7. package/components/data-table/DataTable.tsx +72 -45
  8. package/components/data-table/use-data-table.ts +57 -3
  9. package/components/index.ts +2 -0
  10. package/db.ts +9 -0
  11. package/dist/admin.js +13 -13
  12. package/dist/compile/analyze.js +7 -2
  13. package/dist/compile/entities.js +163 -16
  14. package/dist/compile/model.d.ts +7 -3
  15. package/dist/compile/openapi.js +13 -3
  16. package/dist/compile/program.d.ts +2 -1
  17. package/dist/compile/program.js +7 -2
  18. package/dist/define/auth.d.ts +27 -0
  19. package/dist/define/auth.js +42 -0
  20. package/dist/define/db.d.ts +24 -1
  21. package/dist/define/db.js +1 -1
  22. package/dist/define/index.d.ts +19 -2
  23. package/dist/define/manifest.js +10 -1
  24. package/dist/define/markers.d.ts +80 -0
  25. package/dist/define/markers.js +37 -0
  26. package/dist/define/types.d.ts +10 -0
  27. package/dist/define/types.js +26 -0
  28. package/dist/wasm/rastack_wasm.js +1 -1
  29. package/dist/wasm/rastack_wasm_bg.wasm +0 -0
  30. package/package.json +1 -1
  31. package/provider/index.ts +1 -5
  32. package/provider/provider.tsx +42 -11
  33. package/provider/types.ts +14 -2
  34. package/provider/warehouse.ts +4 -3
  35. package/provider/wasm.ts +3 -1
  36. package/runtime.ts +1 -0
  37. package/src/compile/analyze.ts +15 -3
  38. package/src/compile/entities.ts +193 -19
  39. package/src/compile/model.ts +8 -9
  40. package/src/compile/openapi.ts +11 -3
  41. package/src/compile/program.ts +10 -3
  42. package/src/define/auth.ts +28 -0
  43. package/src/define/db.ts +26 -4
  44. package/src/define/index.ts +20 -2
  45. package/src/define/manifest.ts +10 -1
  46. package/src/define/markers.ts +83 -0
  47. package/src/define/types.ts +11 -0
  48. package/test/auth.spec.ts +207 -0
  49. package/test/compile.spec.ts +18 -7
  50. package/test/components.spec.ts +116 -0
  51. package/test/entities.spec.ts +123 -6
  52. package/types.ts +7 -2
  53. package/wasm/rastack_wasm.js +1 -1
  54. package/wasm/rastack_wasm_bg.wasm +0 -0
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ /**
3
+ * Entity-level authorization markers — auth as *TypeScript types*.
4
+ *
5
+ * Class and interface entities have no options bag, so their access policy is
6
+ * expressed the same way their columns are: in the type system. Attach a
7
+ * marker with `implements` (classes) or `extends` (interfaces) and the
8
+ * compiler reads the policy straight off the heritage clause:
9
+ *
10
+ * ```ts
11
+ * import { AdminGroups, IsAuthenticated, Owner, Group } from "rastack/auth";
12
+ *
13
+ * // Per-user rows: RLS on `owner`, staff sees everything.
14
+ * export class Itinerary implements IsAuthenticated, AdminGroups<"staff"> {
15
+ * owner!: string & Owner;
16
+ * notes!: string;
17
+ * }
18
+ *
19
+ * // Group-owned dataset: rows belong to a team and are *physically stored*
20
+ * // under that team's own storage prefix (groups/{team}/…).
21
+ * export class Report implements IsAuthenticated, AdminGroups<"staff"> {
22
+ * team!: string & Group;
23
+ * title!: string;
24
+ * }
25
+ * ```
26
+ *
27
+ * The markers are phantom types: every property is optional and never exists
28
+ * at runtime, so `implements` needs no members and instances stay plain data.
29
+ * Each marker family uses its own phantom key, so markers combine freely —
30
+ * while two *permission* markers on one entity conflict in the type checker,
31
+ * which is exactly right.
32
+ *
33
+ * JSDoc equivalents exist for every marker (`@rastackPermission`,
34
+ * `@rastackAdminGroups staff, ops`, `@rastackScope owner|group|shared`, and
35
+ * field-level `@owner` / `@group`), mirroring the column-constraint tags.
36
+ */
37
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The `rastack/types` compatibility surface — one module aliasing the two
3
+ * split authoring subpaths: `rastack/db` (the `DbConfig` column brands) and
4
+ * `rastack/auth` (the entity-level auth markers + `Owner`/`Group` brands).
5
+ * Prefer those; everything stays re-exported here so existing imports keep
6
+ * working. The compiler maps all three specifiers (see `compile/program.ts`);
7
+ * the published package re-exports the same modules from `tools/types.ts`.
8
+ */
9
+ export * from "./db";
10
+ export * from "./markers";
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ /**
3
+ * The `rastack/types` compatibility surface — one module aliasing the two
4
+ * split authoring subpaths: `rastack/db` (the `DbConfig` column brands) and
5
+ * `rastack/auth` (the entity-level auth markers + `Owner`/`Group` brands).
6
+ * Prefer those; everything stays re-exported here so existing imports keep
7
+ * working. The compiler maps all three specifiers (see `compile/program.ts`);
8
+ * the published package re-exports the same modules from `tools/types.ts`.
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
22
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
23
+ };
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ __exportStar(require("./db"), exports);
26
+ __exportStar(require("./markers"), exports);
@@ -375,7 +375,7 @@ function __wbg_get_imports() {
375
375
  return ret;
376
376
  },
377
377
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
378
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 85, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
378
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 83, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
379
379
  const ret = makeMutClosure(arg0, arg1, wasm_bindgen_1a7aa9db5392c486___convert__closures_____invoke___wasm_bindgen_1a7aa9db5392c486___JsValue__core_7d5f0a2ba6a62c33___result__Result_____wasm_bindgen_1a7aa9db5392c486___JsError___true_);
380
380
  return ret;
381
381
  },
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rastack",
3
- "version": "0.0.51",
3
+ "version": "0.0.52",
4
4
  "description": "",
5
5
  "main": "runtime.ts",
6
6
  "types": "runtime.ts",
package/provider/index.ts CHANGED
@@ -1,10 +1,6 @@
1
1
  export * from "./types";
2
2
  export * from "./provider";
3
- export {
4
- createWasmAdapter,
5
- createEngine,
6
- defaultEngineLoader,
7
- } from "./wasm";
3
+ export { createWasmAdapter, createEngine, defaultEngineLoader } from "./wasm";
8
4
  export {
9
5
  WarehousePersistence,
10
6
  seedFromIceberg,
@@ -33,6 +33,12 @@ import {
33
33
  isAccessRevoked,
34
34
  purgeIdentityCache,
35
35
  } from "../cache";
36
+ import {
37
+ RastackAuthProvider,
38
+ userAuth,
39
+ type ManifestLike,
40
+ type RastackAuth,
41
+ } from "../auth";
36
42
  import { toManifest } from "../src/define/manifest";
37
43
  import { getGlobalManifest } from "../hooks/manifest";
38
44
 
@@ -74,17 +80,29 @@ export function RAStackProvider({
74
80
  config.onStatusChange?.(next);
75
81
  };
76
82
 
83
+ // One resolved identity, whichever way it was supplied: the typed `auth`
84
+ // object, or the raw `identity` / `groups` strings (which win when both are
85
+ // given). This is what the engine runs as, what namespaces local bytes, and
86
+ // what the component tree gates its affordances with.
87
+ const auth: RastackAuth | undefined =
88
+ config.auth ??
89
+ (config.identity != null
90
+ ? userAuth(config.identity, config.groups ?? [])
91
+ : undefined);
92
+ const identity = config.identity ?? auth?.sub;
93
+ const groups = config.groups ?? auth?.groups ?? [];
94
+
77
95
  // Locally persisted state is namespaced per identity, so two accounts on
78
96
  // one device never share bytes.
79
97
  const warehouseDb = withIdentity(
80
98
  config.databaseName || "rastack-warehouse",
81
- config.identity,
99
+ identity,
82
100
  );
83
101
 
84
102
  /** Delete everything this identity persisted locally. */
85
103
  const purgeLocal = async () => {
86
104
  if (cacheStorageRef.current) {
87
- await purgeIdentityCache(cacheStorageRef.current, config.identity);
105
+ await purgeIdentityCache(cacheStorageRef.current, identity);
88
106
  }
89
107
  await deleteIndexedDb(warehouseDb);
90
108
  // Replicated rows in the sync engine's store are derived from the same
@@ -136,8 +154,8 @@ export function RAStackProvider({
136
154
 
137
155
  // Run the in-browser engine as the signed-in identity so owner stamping
138
156
  // and row-level security match the server exactly.
139
- if (config.identity) {
140
- engine.setAuth?.(config.identity, config.groups ?? []);
157
+ if (identity) {
158
+ engine.setAuth?.(identity, groups);
141
159
  }
142
160
 
143
161
  // Warehouse persistence: IndexedDB for `local`, an S3 sink for `s3`.
@@ -166,7 +184,7 @@ export function RAStackProvider({
166
184
  const cache = new IcebergFileCache({
167
185
  storage,
168
186
  source: config.fileSource ?? httpFileSource(base),
169
- identity: config.identity,
187
+ identity,
170
188
  maxOfflineMs: config.maxOfflineMs,
171
189
  });
172
190
  await cache.seed(engine, resources);
@@ -191,7 +209,10 @@ export function RAStackProvider({
191
209
  persistRef.current = persist;
192
210
 
193
211
  engineRef.current = engine;
194
- configureApi({ baseURL: "", adapter: createWasmAdapter(engine, persist) });
212
+ configureApi({
213
+ baseURL: "",
214
+ adapter: createWasmAdapter(engine, persist),
215
+ });
195
216
  setStatusSafe("ready");
196
217
  }
197
218
 
@@ -209,18 +230,21 @@ export function RAStackProvider({
209
230
  engineRef.current = null;
210
231
  };
211
232
  // eslint-disable-next-line react-hooks/exhaustive-deps
212
- }, [config.mode, config.baseURL, config.warehouseBaseUrl, config.identity]);
233
+ }, [config.mode, config.baseURL, config.warehouseBaseUrl, identity]);
213
234
 
214
235
  const client = useMemo<RastackClient>(
215
236
  () => ({
216
237
  mode: config.mode,
217
238
  status,
239
+ auth,
218
240
  get engine() {
219
241
  return engineRef.current;
220
242
  },
221
243
  async exportWarehouse() {
222
244
  if (!engineRef.current) {
223
- throw new Error("exportWarehouse is only available in local/s3 mode.");
245
+ throw new Error(
246
+ "exportWarehouse is only available in local/s3 mode.",
247
+ );
224
248
  }
225
249
  return engineRef.current.exportWarehouse();
226
250
  },
@@ -239,13 +263,18 @@ export function RAStackProvider({
239
263
  },
240
264
  }),
241
265
  // eslint-disable-next-line react-hooks/exhaustive-deps
242
- [config.mode, status, config.identity],
266
+ [config.mode, status, identity],
243
267
  );
244
268
 
245
269
  return (
246
270
  <RastackContext.Provider value={client}>
247
271
  <RastackManifestProvider manifest={manifest}>
248
- {children}
272
+ <RastackAuthProvider
273
+ auth={auth}
274
+ manifest={manifest as ManifestLike | undefined}
275
+ >
276
+ {children}
277
+ </RastackAuthProvider>
249
278
  </RastackManifestProvider>
250
279
  </RastackContext.Provider>
251
280
  );
@@ -279,7 +308,9 @@ function withIdentity(name: string, identity?: string): string {
279
308
  export function useRastackClient(): RastackClient {
280
309
  const client = useContext(RastackContext);
281
310
  if (!client) {
282
- throw new Error("useRastackClient must be used within a <RAStackProvider>.");
311
+ throw new Error(
312
+ "useRastackClient must be used within a <RAStackProvider>.",
313
+ );
283
314
  }
284
315
  return client;
285
316
  }
package/provider/types.ts CHANGED
@@ -68,14 +68,24 @@ export interface RAStackProviderConfig {
68
68
  getToken?: () => string | Promise<string>;
69
69
 
70
70
  // -- identity & local-data security (all modes) --
71
+ /**
72
+ * The signed-in identity as a typed {@link import("../auth").RastackAuth} —
73
+ * the one auth object the whole stack shares. Supplies `identity`/`groups`
74
+ * when those aren't set, runs the WASM engine as this identity, and is
75
+ * provided to the component tree (via `rastack/auth`'s context) so
76
+ * `<AutoForm>` / `<DataTable>` gate their affordances against each
77
+ * resource's `permission` + `access` policy automatically.
78
+ */
79
+ auth?: import("../auth").RastackAuth;
71
80
  /**
72
81
  * The signed-in identity — the verified Cognito `sub`. It namespaces every
73
82
  * locally persisted byte (Iceberg file cache, warehouse snapshot DB), so
74
83
  * accounts sharing a device can't read each other's data, and it is the
75
- * subject the WASM engine stamps/filters owner rows with.
84
+ * subject the WASM engine stamps/filters owner rows with. Defaults to
85
+ * `auth.sub`.
76
86
  */
77
87
  identity?: string;
78
- /** Verified group claims (`cognito:groups`) for the WASM engine's auth. */
88
+ /** Verified group claims (`cognito:groups`). Defaults to `auth.groups`. */
79
89
  groups?: string[];
80
90
  /**
81
91
  * Fired when the data source reports this identity's access has been
@@ -166,6 +176,8 @@ export type RadStatus =
166
176
  export interface RastackClient {
167
177
  mode: RadMode;
168
178
  status: RadStatus;
179
+ /** The resolved identity this client runs as (from `auth` or `identity`). */
180
+ auth?: import("../auth").RastackAuth;
169
181
  /** The live WASM engine (null in `remote` mode). */
170
182
  engine: RastackEngine | null;
171
183
  /** Snapshot the local warehouse to a portable blob (WASM modes only). */
@@ -26,8 +26,7 @@ export class WarehousePersistence {
26
26
  private db: Promise<IDBDatabase> | null;
27
27
 
28
28
  constructor(private databaseName: string) {
29
- this.db =
30
- typeof indexedDB !== "undefined" ? openDb(databaseName) : null;
29
+ this.db = typeof indexedDB !== "undefined" ? openDb(databaseName) : null;
31
30
  }
32
31
 
33
32
  get enabled(): boolean {
@@ -109,7 +108,9 @@ export async function seedFromIceberg(
109
108
  if (!metaBytes) continue;
110
109
  await engine.seedFile(metaKey, metaBytes);
111
110
 
112
- const meta = JSON.parse(decoder.decode(metaBytes)) as { data_files?: string[] };
111
+ const meta = JSON.parse(decoder.decode(metaBytes)) as {
112
+ data_files?: string[];
113
+ };
113
114
  for (const rel of meta.data_files || []) {
114
115
  const key = `${prefix}/${rel}`;
115
116
  const bytes = await fetchBytes(base + key);
package/provider/wasm.ts CHANGED
@@ -13,7 +13,9 @@ const runtimeImport: (specifier: string) => Promise<any> =
13
13
  new Function("s", "return import(s)") as any;
14
14
 
15
15
  export const defaultEngineLoader: RastackEngineLoader = async () => {
16
- const mod: RastackWasmModule = await runtimeImport("rastack/wasm/rastack_wasm.js");
16
+ const mod: RastackWasmModule = await runtimeImport(
17
+ "rastack/wasm/rastack_wasm.js",
18
+ );
17
19
  const init = mod.default || mod.init;
18
20
  if (typeof init === "function") {
19
21
  await init();
package/runtime.ts CHANGED
@@ -2,3 +2,4 @@ export * from "./hooks";
2
2
  export * from "./types";
3
3
  export * from "./provider";
4
4
  export * from "./cache";
5
+ export * from "./auth";
@@ -69,7 +69,9 @@ export function analyzeDsl(
69
69
 
70
70
  // Deterministic order: app, then model.
71
71
  resources.sort((a, b) =>
72
- a.app === b.app ? a.model.localeCompare(b.model) : a.app.localeCompare(b.app),
72
+ a.app === b.app
73
+ ? a.model.localeCompare(b.model)
74
+ : a.app.localeCompare(b.app),
73
75
  );
74
76
  types.sort((a, b) => a.key.localeCompare(b.key));
75
77
  return { resources, types };
@@ -114,7 +116,12 @@ function analyzeResourceCall(
114
116
  const app = stringLiteralArg(call.arguments[0]);
115
117
  const model = stringLiteralArg(call.arguments[1]);
116
118
  const fieldsArg = call.arguments[2];
117
- if (!app || !model || !fieldsArg || !ts.isObjectLiteralExpression(fieldsArg)) {
119
+ if (
120
+ !app ||
121
+ !model ||
122
+ !fieldsArg ||
123
+ !ts.isObjectLiteralExpression(fieldsArg)
124
+ ) {
118
125
  return undefined;
119
126
  }
120
127
 
@@ -178,7 +185,12 @@ function analyzeField(
178
185
  }
179
186
 
180
187
  // Scalar: the field type carries a `__rastackScalar` string-literal discriminant.
181
- const scalar = literalTypeProp(fieldType, "__rastackScalar", checker, initializer);
188
+ const scalar = literalTypeProp(
189
+ fieldType,
190
+ "__rastackScalar",
191
+ checker,
192
+ initializer,
193
+ );
182
194
  if (!scalar) return undefined;
183
195
 
184
196
  const field: FieldModel = { name, type: scalar as ScalarType };