@arcote.tech/arc 0.5.1 → 0.5.5

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.
@@ -19,6 +19,17 @@ export declare class AuthAdapter {
19
19
  * Auto-persists to localStorage when available.
20
20
  */
21
21
  setToken(token: string | null, scope?: string): void;
22
+ /**
23
+ * Set an already-decoded token for a scope, bypassing JWT parsing.
24
+ *
25
+ * Used by listener auth reconstruction: events carry decoded auth context
26
+ * (tokenName + params) which has been validated at emit time. This method
27
+ * restores that context into a fresh AuthAdapter without requiring the
28
+ * original raw JWT.
29
+ *
30
+ * Does NOT touch localStorage — purely in-memory restoration.
31
+ */
32
+ setDecoded(decoded: DecodedToken, scope?: string): void;
22
33
  /**
23
34
  * Load all persisted scope tokens from localStorage.
24
35
  * Call once on app init before any queries.
@@ -27,6 +27,11 @@ export interface StoredEvent {
27
27
  type: string;
28
28
  payload: string;
29
29
  createdAt: string;
30
+ /**
31
+ * JSON-serialized EventAuthContext snapshot from the mutation that emitted
32
+ * this event. Null for system events / cron / seed (no active auth scope).
33
+ */
34
+ authContext: string | null;
30
35
  }
31
36
  /** Event tag record in the event_tags table */
32
37
  export interface StoredEventTag {
@@ -7,11 +7,13 @@
7
7
  * - Handles reconnection and sync state
8
8
  * - Manages per-scope token caching
9
9
  */
10
+ import type { EventAuthContext } from "../context-element/event/instance";
10
11
  export interface SyncableEvent {
11
12
  localId: string;
12
13
  type: string;
13
14
  payload: any;
14
15
  createdAt: string;
16
+ authContext: EventAuthContext | null;
15
17
  }
16
18
  export interface ReceivedEvent {
17
19
  localId: string;
@@ -20,6 +22,7 @@ export interface ReceivedEvent {
20
22
  payload: any;
21
23
  createdAt: string;
22
24
  clientId: string;
25
+ authContext: EventAuthContext | null;
23
26
  }
24
27
  import type { ContextDescriptor } from "../model/context-accessor";
25
28
  type EventWireState = "disconnected" | "connecting" | "connected";
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { ArcIdAny } from "../../elements/id";
8
8
  import type { ArcObjectAny } from "../../elements/object";
9
+ import type { ArcContextElement } from "../context-element";
9
10
  import type { Simplify } from "../../utils";
10
11
  import type { ArcEventAny } from "../event/event";
11
12
  import type { ArcEventInstance } from "../event/instance";
@@ -34,6 +35,8 @@ export interface AggregateMutateMethodEntry {
34
35
  readonly handler: Function | false | null;
35
36
  readonly result?: any;
36
37
  readonly cronExpression?: string;
38
+ readonly queryElements?: ArcContextElement<any>[];
39
+ readonly mutationElements?: ArcContextElement<any>[];
37
40
  }
38
41
  /**
39
42
  * Cron method entry extracted at build time.
@@ -111,7 +111,7 @@ export declare class ArcAggregateElement<Name extends string = string, Id extend
111
111
  data: any[];
112
112
  version: number;
113
113
  });
114
- protectBy<T extends ArcTokenAny>(token: T, protectionFn: ViewProtectionFn<any>): ArcAggregateElement<Name, Id, Schema, Events, MutateMethods, QueryMethods>;
114
+ protectBy<T extends ArcTokenAny>(token: T, protectionFn: ViewProtectionFn<T>): ArcAggregateElement<Name, Id, Schema, Events, MutateMethods, QueryMethods>;
115
115
  event<const EventName extends string, PayloadShape extends ArcRawShape>(eventName: EventName, payload: PayloadShape, handler: (ctx: ArcViewHandlerContext<Id, Schema>, event: ArcEventInstance<InlineArcEvent<EventName, PayloadShape>>) => Promise<void>): ArcAggregateElement<Name, Id, Schema, [
116
116
  ...Events,
117
117
  AggregateEventEntry<InlineArcEvent<EventName, PayloadShape>, Id, Schema, false>
@@ -196,8 +196,9 @@ export declare class ArcAggregateElement<Name extends string = string, Id extend
196
196
  get cronMethods(): AggregateCronMethodEntry[];
197
197
  queryContext(adapters: ModelAdapters): AggregateQueryContext<QueryMethods>;
198
198
  private buildPrivateQuery;
199
- private buildUnrestrictedQuery;
199
+ private isScopeDenied;
200
200
  private getAuth;
201
+ private getScopeRestrictions;
201
202
  mutateContext(adapters: ModelAdapters): AggregateMutateMethodContext<MutateMethods>;
202
203
  databaseStoreSchema(): DatabaseStoreSchema;
203
204
  getSeeds(): {
@@ -11,5 +11,5 @@ export { aggregate } from "./aggregate-builder";
11
11
  export { ArcAggregateElement, type AggregateConstructor, type AggregateConstructorAny, type AggregateData, type AggregateInstanceCtx, type AggregateRow, } from "./aggregate-element";
12
12
  export { AggregateBase } from "./aggregate-base";
13
13
  export type { AggregateCronMethodEntry, AggregateEventEntry, AggregateEventHandler, AggregateMutateMethodContext, AggregateMutateMethodEntry, AggregateQueryContext, AggregateQueryMethodEntry, AggregateStaticConfig, } from "./aggregate-data";
14
- export type { ArcEventPayload } from "../event/instance";
14
+ export type { ArcEventPayload, EventAuthContext, EventMetadata } from "../event/instance";
15
15
  //# sourceMappingURL=index.d.ts.map
@@ -1,5 +1,5 @@
1
1
  export { ArcEvent, event, type ArcEventAny } from "./event";
2
2
  export type { ArcEventData } from "./event-data";
3
- export type { ArcEventInstance, ArcEventPayload, EventMetadata, } from "./instance";
3
+ export type { ArcEventInstance, ArcEventPayload, EventAuthContext, EventMetadata, } from "./instance";
4
4
  export type { ArcObjectAny, ArcRawShape } from "../../elements/object";
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1,11 +1,30 @@
1
1
  import type { $type } from "../../utils/types/get-type";
2
2
  import type { ArcEventAny } from "./event";
3
+ /**
4
+ * Auth context captured at event emission time.
5
+ * Stored alongside the event in the event store so async listeners can
6
+ * reconstruct the original protection scope without losing security guarantees.
7
+ *
8
+ * Trust model: the event store is trusted (same threat surface as the
9
+ * aggregate state DB). No cryptographic re-verification — payload is
10
+ * authoritative because only authorized mutations could have written it.
11
+ */
12
+ export type EventAuthContext = {
13
+ tokenName: string;
14
+ params: Record<string, any>;
15
+ };
3
16
  /**
4
17
  * Event metadata attached to every event instance
5
18
  */
6
19
  export type EventMetadata = {
7
20
  id: string;
8
21
  createdAt: Date;
22
+ /**
23
+ * Auth context that authorized the mutation which emitted this event.
24
+ * `null` for system events emitted without an active auth scope —
25
+ * listeners with `protectBy()` will deny access for null-auth events.
26
+ */
27
+ authContext: EventAuthContext | null;
9
28
  };
10
29
  /**
11
30
  * Event instance type - represents an actual event occurrence
@@ -1,18 +1,29 @@
1
1
  import type { ArcObjectAny } from "../../elements/object";
2
2
  import type { ArcToken, ArcTokenAny } from "../../token/token";
3
- import type { TokenInstanceAny } from "../../token/token-instance";
3
+ import type { TokenInstance, TokenInstanceAny } from "../../token/token-instance";
4
4
  import type { $type } from "../../utils/types/get-type";
5
5
  import type { ArcContextElement } from "../context-element";
6
6
  import type { ElementContext } from "../element-context";
7
+ /**
8
+ * Extract typed TokenInstance from a token definition.
9
+ *
10
+ * Given `ArcToken<"workspace", { workspaceId: ArcId, role: ArcString }, ...>`,
11
+ * returns `TokenInstance<{ workspaceId: string, role: string }, RuleNames>`.
12
+ */
13
+ export type TypedTokenInstance<T extends ArcTokenAny> = T extends ArcToken<any, infer ParamsShape, any, infer Rules> ? TokenInstance<$type<import("../../elements/object").ArcObject<ParamsShape>>, Rules extends Record<string, any> ? Extract<keyof Rules, string> : string> : TokenInstanceAny;
7
14
  /**
8
15
  * Unified protection check function.
9
16
  *
17
+ * Callback receives a **typed** TokenInstance — `token.params.workspaceId`
18
+ * has full autocomplete and type safety.
19
+ *
10
20
  * Return value determines behavior:
11
21
  * - `false` → access denied
12
22
  * - `true` → access allowed, no data filter
13
23
  * - `Record` → access allowed + merged into WHERE clause (query context)
24
+ * + validated on writes (scope enforcement)
14
25
  */
15
- export type FnProtectionCheck<T extends ArcTokenAny> = (tokenInstance: TokenInstanceAny) => boolean | Record<string, unknown> | Promise<boolean | Record<string, unknown>>;
26
+ export type FnProtectionCheck<T extends ArcTokenAny> = (tokenInstance: TypedTokenInstance<T>) => boolean | Record<string, unknown> | Promise<boolean | Record<string, unknown>>;
16
27
  /**
17
28
  * Protection configuration for an ArcFunction.
18
29
  */
@@ -76,6 +76,20 @@ export declare class ArcListener<const Data extends ArcListenerData> extends Arc
76
76
  get isAsync(): boolean;
77
77
  init(environment: ArcEnvironment, adapters: ModelAdapters): Promise<void>;
78
78
  private handleEvent;
79
+ /**
80
+ * Build scoped adapters for the listener handler.
81
+ *
82
+ * Sync listeners run inline with the originating mutation and inherit the
83
+ * caller's auth context directly. Async listeners run after the mutation
84
+ * completes and lose request scope — they reconstruct auth from the
85
+ * `authContext` snapshotted into the event at emit time
86
+ * (see aggregate-element.ts emit handler).
87
+ *
88
+ * Events with `authContext: null` (system events / cron / seed) leave the
89
+ * adapters without auth — listeners with `protectBy()` will deny access
90
+ * downstream at the protection layer.
91
+ */
92
+ private buildScopedAdapters;
79
93
  destroy(): void;
80
94
  }
81
95
  export declare function listener<const Name extends string>(name: Name): ArcListener<{
@@ -1,6 +1,6 @@
1
1
  import type { WhereCondition } from "../../data-storage/find-options";
2
2
  import type { ArcObjectAny } from "../../elements/object";
3
- import type { ArcTokenAny } from "../../token/token";
3
+ import type { ArcToken, ArcTokenAny } from "../../token/token";
4
4
  import type { ArcContextElement } from "../context-element";
5
5
  /**
6
6
  * Protection config for views
@@ -9,11 +9,11 @@ import type { ArcContextElement } from "../context-element";
9
9
  */
10
10
  export type ViewProtectionConfig = WhereCondition | false;
11
11
  /**
12
- * Protection function for views
13
- * Receives token params and returns read conditions (where clause)
14
- * Return false to deny access entirely
12
+ * Protection function for views/aggregates.
13
+ * Receives typed token params and returns read/write conditions (where clause).
14
+ * Return false to deny access entirely.
15
15
  */
16
- export type ViewProtectionFn<TokenParams> = (params: TokenParams) => ViewProtectionConfig;
16
+ export type ViewProtectionFn<T extends ArcTokenAny = ArcTokenAny> = (params: T extends ArcToken<any, infer P, any, any> ? import("../../utils/types/get-type").$type<import("../../elements/object").ArcObject<P>> : Record<string, any>) => ViewProtectionConfig;
17
17
  /**
18
18
  * View protection configuration
19
19
  */
@@ -5,7 +5,7 @@ import type { ArcTokenAny } from "../../token/token";
5
5
  import type { TokenInstanceAny } from "../../token/token-instance";
6
6
  import type { Merge } from "../../utils";
7
7
  import type { ArcViewHandlerContext, ArcViewItem, ArcViewQueryContext } from "./view-context";
8
- import type { ArcViewData, ViewProtectionFn } from "./view-data";
8
+ import type { ArcViewData, ViewProtection, ViewProtectionFn } from "./view-data";
9
9
  /**
10
10
  * Arc View - Read-optimized projection of events
11
11
  *
@@ -104,6 +104,7 @@ export declare class ArcView<const Data extends ArcViewData> extends ArcContextE
104
104
  * @returns Context object with find/findOne methods
105
105
  */
106
106
  queryContext(adapters: any): ArcViewQueryContext<Data["id"], Data["schema"]>;
107
+ private resolveProtection;
107
108
  /**
108
109
  * Get event handlers for this view
109
110
  * Used by the framework to set up event listeners
@@ -144,7 +145,7 @@ export declare class ArcView<const Data extends ArcViewData> extends ArcContextE
144
145
  /**
145
146
  * Get all protection configurations
146
147
  */
147
- get protections(): import("./view-data").ViewProtection[];
148
+ get protections(): ViewProtection[];
148
149
  /**
149
150
  * Get protection config for a specific token instance
150
151
  *
@@ -9,5 +9,6 @@ export * from "./query-result-resolver";
9
9
  export * from "./schema-extraction";
10
10
  export * from "./store-state-fork";
11
11
  export * from "./store-state-master";
12
+ export * from "./scoped-data-storage";
12
13
  export * from "./store-state.abstract";
13
14
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,35 @@
1
+ import { DataStorage, type QueryListenerCallback, type StoreStateChange } from "./data-storage.abstract";
2
+ import type { StoreState } from "./store-state.abstract";
3
+ import type { FindOptions } from "./find-options";
4
+ import type { ForkedDataStorage } from "./data-storage-forked";
5
+ import type { ReadTransaction, ReadWriteTransaction } from "./database-adapter";
6
+ export type ScopeRestrictions = Record<string, unknown>;
7
+ export type StorePermission = "read" | "read-write";
8
+ export declare class ScopedStore<Item extends {
9
+ _id: string;
10
+ }> {
11
+ #private;
12
+ constructor(inner: StoreState<Item>, restrictions: ScopeRestrictions, canWrite: boolean);
13
+ get storeName(): string;
14
+ find(options?: FindOptions<Item>, listener?: QueryListenerCallback<Item>): Promise<Item[]>;
15
+ set(item: Item): Promise<void>;
16
+ remove(id: string): Promise<void>;
17
+ modify(id: string, data: Partial<Item>): Promise<{
18
+ from: Item | null;
19
+ to: Item | null;
20
+ }>;
21
+ applyChanges(changes: StoreStateChange<Item>[]): Promise<void>;
22
+ unsubscribe(listener: QueryListenerCallback<Item>): void;
23
+ }
24
+ export declare class ScopedDataStorage extends DataStorage {
25
+ #private;
26
+ constructor(inner: DataStorage, allowedStores: Map<string, StorePermission>, restrictions: ScopeRestrictions);
27
+ getStore<Item extends {
28
+ _id: string;
29
+ }>(storeName: string): StoreState<Item>;
30
+ fork(): ForkedDataStorage;
31
+ getReadTransaction(): Promise<ReadTransaction>;
32
+ getReadWriteTransaction(): Promise<ReadWriteTransaction>;
33
+ }
34
+ export declare function createScopedDataStorage(inner: DataStorage, queryElementNames: string[], mutateElementNames: string[], restrictions: ScopeRestrictions): ScopedDataStorage;
35
+ //# sourceMappingURL=scoped-data-storage.d.ts.map
package/dist/index.js CHANGED
@@ -50,6 +50,9 @@ class AuthAdapter {
50
50
  localStorage.removeItem(TOKEN_PREFIX + scope);
51
51
  }
52
52
  }
53
+ setDecoded(decoded, scope = "default") {
54
+ this.scopes.set(scope, { raw: "", decoded });
55
+ }
53
56
  loadPersisted() {
54
57
  if (!hasLocalStorage())
55
58
  return;
@@ -325,7 +328,8 @@ class EventWire {
325
328
  localId: e.localId,
326
329
  type: e.type,
327
330
  payload: e.payload,
328
- createdAt: e.createdAt
331
+ createdAt: e.createdAt,
332
+ authContext: e.authContext
329
333
  }))
330
334
  }));
331
335
  }
@@ -541,11 +545,13 @@ class LocalEventPublisher {
541
545
  }
542
546
  async publish(event) {
543
547
  const allChanges = [];
548
+ const eventAuthContext = event.authContext ?? null;
544
549
  const storedEvent = {
545
550
  _id: event.id,
546
551
  type: event.type,
547
552
  payload: JSON.stringify(event.payload),
548
- createdAt: event.createdAt.toISOString()
553
+ createdAt: event.createdAt.toISOString(),
554
+ authContext: eventAuthContext ? JSON.stringify(eventAuthContext) : null
549
555
  };
550
556
  allChanges.push({
551
557
  store: EVENT_TABLES.events,
@@ -1270,6 +1276,122 @@ function object(element) {
1270
1276
  return new ArcObject(element);
1271
1277
  }
1272
1278
 
1279
+ // src/data-storage/data-storage.abstract.ts
1280
+ class DataStorage {
1281
+ async commitChanges(changes) {
1282
+ await Promise.all(changes.map(({ store, changes: changes2 }) => this.getStore(store).applyChanges(changes2)));
1283
+ }
1284
+ }
1285
+
1286
+ // src/data-storage/scoped-data-storage.ts
1287
+ class ScopedStore {
1288
+ #inner;
1289
+ #restrictions;
1290
+ #canWrite;
1291
+ #storeName;
1292
+ constructor(inner, restrictions, canWrite) {
1293
+ this.#inner = inner;
1294
+ this.#restrictions = restrictions;
1295
+ this.#canWrite = canWrite;
1296
+ this.#storeName = inner.storeName;
1297
+ }
1298
+ get storeName() {
1299
+ return this.#storeName;
1300
+ }
1301
+ async find(options, listener) {
1302
+ const restricted = this.#applyReadRestrictions(options);
1303
+ return this.#inner.find(restricted, listener);
1304
+ }
1305
+ async set(item) {
1306
+ this.#assertWriteAccess();
1307
+ this.#validateScopeFields(item);
1308
+ return this.#inner.set(item);
1309
+ }
1310
+ async remove(id) {
1311
+ this.#assertWriteAccess();
1312
+ return this.#inner.remove(id);
1313
+ }
1314
+ async modify(id, data) {
1315
+ this.#assertWriteAccess();
1316
+ this.#validateScopeFields(data);
1317
+ return this.#inner.modify(id, data);
1318
+ }
1319
+ async applyChanges(changes) {
1320
+ this.#assertWriteAccess();
1321
+ for (const change of changes) {
1322
+ if (change.type === "set") {
1323
+ this.#validateScopeFields(change.data);
1324
+ } else if (change.type === "modify") {
1325
+ this.#validateScopeFields(change.data);
1326
+ }
1327
+ }
1328
+ return this.#inner.applyChanges(changes);
1329
+ }
1330
+ unsubscribe(listener) {
1331
+ this.#inner.unsubscribe(listener);
1332
+ }
1333
+ #applyReadRestrictions(options) {
1334
+ if (Object.keys(this.#restrictions).length === 0) {
1335
+ return options ?? {};
1336
+ }
1337
+ return {
1338
+ ...options,
1339
+ where: { ...options?.where ?? {}, ...this.#restrictions }
1340
+ };
1341
+ }
1342
+ #assertWriteAccess() {
1343
+ if (!this.#canWrite) {
1344
+ throw new Error(`Scope violation: write access denied to store "${this.#storeName}" (read-only)`);
1345
+ }
1346
+ }
1347
+ #validateScopeFields(data) {
1348
+ for (const [key, value] of Object.entries(this.#restrictions)) {
1349
+ if (key in data && data[key] !== value) {
1350
+ throw new Error(`Scope violation: field "${key}" must be "${value}", got "${data[key]}" in store "${this.#storeName}"`);
1351
+ }
1352
+ }
1353
+ }
1354
+ }
1355
+
1356
+ class ScopedDataStorage extends DataStorage {
1357
+ #inner;
1358
+ #allowedStores;
1359
+ #restrictions;
1360
+ constructor(inner, allowedStores, restrictions) {
1361
+ super();
1362
+ this.#inner = inner;
1363
+ this.#allowedStores = allowedStores;
1364
+ this.#restrictions = restrictions;
1365
+ }
1366
+ getStore(storeName) {
1367
+ const permission = this.#allowedStores.get(storeName);
1368
+ if (!permission) {
1369
+ throw new Error(`Scope violation: access denied to store "${storeName}" (not declared in query/mutate)`);
1370
+ }
1371
+ const inner = this.#inner.getStore(storeName);
1372
+ return new ScopedStore(inner, this.#restrictions, permission === "read-write");
1373
+ }
1374
+ fork() {
1375
+ return this.#inner.fork();
1376
+ }
1377
+ getReadTransaction() {
1378
+ return this.#inner.getReadTransaction();
1379
+ }
1380
+ getReadWriteTransaction() {
1381
+ return this.#inner.getReadWriteTransaction();
1382
+ }
1383
+ }
1384
+ function createScopedDataStorage(inner, queryElementNames, mutateElementNames, restrictions) {
1385
+ const allowedStores = new Map;
1386
+ for (const name of queryElementNames) {
1387
+ allowedStores.set(name, "read");
1388
+ }
1389
+ for (const name of mutateElementNames) {
1390
+ allowedStores.set(name, "read-write");
1391
+ }
1392
+ return new ScopedDataStorage(inner, allowedStores, restrictions);
1393
+ }
1394
+
1273
1395
  // src/elements/abstract-primitive.ts
1274
1396
  class ArcPrimitive extends ArcAbstract {
1275
1397
  serialize(value) {
@@ -1460,6 +1582,56 @@ class ArcContextElement extends ArcFragmentBase {
1460
1582
  }
1461
1583
  }
1462
1584
 
1585
+ // src/context-element/element-context.ts
1586
+ function buildElementContext(queryElements, mutationElements, adapters) {
1587
+ const queryMap = new Map;
1588
+ const mutateMap = new Map;
1589
+ const queryProps = {};
1590
+ for (const element of queryElements) {
1591
+ if (element.queryContext) {
1592
+ const ctx = element.queryContext(adapters);
1593
+ queryProps[element.name] = ctx;
1594
+ queryMap.set(element, ctx);
1595
+ }
1596
+ }
1597
+ const mutateProps = {};
1598
+ for (const element of mutationElements) {
1599
+ if (element.mutateContext) {
1600
+ const ctx = element.mutateContext(adapters);
1601
+ mutateProps[element.name] = ctx;
1602
+ mutateMap.set(element, ctx);
1603
+ }
1604
+ }
1605
+ const queryFn = (element) => {
1606
+ const cached = queryMap.get(element);
1607
+ if (cached)
1608
+ return cached;
1609
+ if (element.queryContext) {
1610
+ const ctx = element.queryContext(adapters);
1611
+ queryMap.set(element, ctx);
1612
+ return ctx;
1613
+ }
1614
+ throw new Error(`Element "${element.name}" has no queryContext`);
1615
+ };
1616
+ Object.assign(queryFn, queryProps);
1617
+ const mutateFn = (element) => {
1618
+ const cached = mutateMap.get(element);
1619
+ if (cached)
1620
+ return cached;
1621
+ if (element.mutateContext) {
1622
+ const ctx = element.mutateContext(adapters);
1623
+ mutateMap.set(element, ctx);
1624
+ return ctx;
1625
+ }
1626
+ throw new Error(`Element "${element.name}" has no mutateContext`);
1627
+ };
1628
+ Object.assign(mutateFn, mutateProps);
1629
+ return {
1630
+ query: queryFn,
1631
+ mutate: mutateFn
1632
+ };
1633
+ }
1634
+
1463
1635
  // src/elements/boolean.ts
1464
1636
  class ArcBoolean extends ArcPrimitive {
1465
1637
  hasToBeTrue() {
@@ -1609,11 +1781,14 @@ class ArcEvent extends ArcContextElement {
1609
1781
  if (!adapters.eventPublisher) {
1610
1782
  throw new Error(`Event "${this.data.name}" cannot be emitted: no eventPublisher adapter available`);
1611
1783
  }
1784
+ const decoded = adapters.authAdapter?.getDecoded() ?? null;
1785
+ const authContext = decoded ? { tokenName: decoded.tokenName, params: decoded.params } : null;
1612
1786
  const event = {
1613
1787
  type: this.data.name,
1614
1788
  payload,
1615
1789
  id: this.eventId.generate(),
1616
- createdAt: new Date
1790
+ createdAt: new Date,
1791
+ authContext
1617
1792
  };
1618
1793
  await adapters.eventPublisher.publish(event);
1619
1794
  }
@@ -1697,56 +1872,6 @@ function event(name, payload) {
1697
1872
  });
1698
1873
  }
1699
1874
 
1700
- // src/context-element/element-context.ts
1701
- function buildElementContext(queryElements, mutationElements, adapters) {
1702
- const queryMap = new Map;
1703
- const mutateMap = new Map;
1704
- const queryProps = {};
1705
- for (const element of queryElements) {
1706
- if (element.queryContext) {
1707
- const ctx = element.queryContext(adapters);
1708
- queryProps[element.name] = ctx;
1709
- queryMap.set(element, ctx);
1710
- }
1711
- }
1712
- const mutateProps = {};
1713
- for (const element of mutationElements) {
1714
- if (element.mutateContext) {
1715
- const ctx = element.mutateContext(adapters);
1716
- mutateProps[element.name] = ctx;
1717
- mutateMap.set(element, ctx);
1718
- }
1719
- }
1720
- const queryFn = (element) => {
1721
- const cached = queryMap.get(element);
1722
- if (cached)
1723
- return cached;
1724
- if (element.queryContext) {
1725
- const ctx = element.queryContext(adapters);
1726
- queryMap.set(element, ctx);
1727
- return ctx;
1728
- }
1729
- throw new Error(`Element "${element.name}" has no queryContext`);
1730
- };
1731
- Object.assign(queryFn, queryProps);
1732
- const mutateFn = (element) => {
1733
- const cached = mutateMap.get(element);
1734
- if (cached)
1735
- return cached;
1736
- if (element.mutateContext) {
1737
- const ctx = element.mutateContext(adapters);
1738
- mutateMap.set(element, ctx);
1739
- return ctx;
1740
- }
1741
- throw new Error(`Element "${element.name}" has no mutateContext`);
1742
- };
1743
- Object.assign(mutateFn, mutateProps);
1744
- return {
1745
- query: queryFn,
1746
- mutate: mutateFn
1747
- };
1748
- }
1749
-
1750
1875
  // src/context-element/function/arc-function.ts
1751
1876
  class ArcFunction {
1752
1877
  data;
@@ -1881,11 +2006,14 @@ class AggregateBase {
1881
2006
  if (!adapters.eventPublisher) {
1882
2007
  throw new Error(`Cannot emit event "${arcEvent.name}": no eventPublisher adapter available`);
1883
2008
  }
2009
+ const decoded = adapters.authAdapter?.getDecoded() ?? null;
2010
+ const authContext = decoded ? { tokenName: decoded.tokenName, params: decoded.params } : null;
1884
2011
  const eventInstance = {
1885
2012
  type: arcEvent.name,
1886
2013
  payload,
1887
2014
  id: `${Date.now()}_${Math.random().toString(36).slice(2)}`,
1888
- createdAt: new Date
2015
+ createdAt: new Date,
2016
+ authContext
1889
2017
  };
1890
2018
  await adapters.eventPublisher.publish(eventInstance);
1891
2019
  }
@@ -1962,7 +2090,9 @@ class ArcAggregateElement extends ArcContextElement {
1962
2090
  const entry = {
1963
2091
  name: methodName,
1964
2092
  params: configuredFn.data.params,
1965
- handler: configuredFn.data.handler
2093
+ handler: configuredFn.data.handler,
2094
+ queryElements: configuredFn.data.queryElements?.length ? configuredFn.data.queryElements : undefined,
2095
+ mutationElements: configuredFn.data.mutationElements?.length ? configuredFn.data.mutationElements : undefined
1966
2096
  };
1967
2097
  return new ArcAggregateElement(this.name, this._id_factory, this.schema, this._events, this._protections, [...this._mutateMethods, entry], this._queryMethods, this._seeds);
1968
2098
  }
@@ -2089,38 +2219,16 @@ class ArcAggregateElement extends ArcContextElement {
2089
2219
  }
2090
2220
  buildPrivateQuery(adapters) {
2091
2221
  const viewName = this.name;
2092
- const protections = this._protections;
2093
- const getReadRestrictions = () => {
2094
- if (protections.length === 0)
2095
- return null;
2096
- if (!adapters.authAdapter)
2097
- return false;
2098
- const decoded = adapters.authAdapter.getDecoded();
2099
- if (!decoded)
2100
- return false;
2101
- for (const protection of protections) {
2102
- if (protection.token.name === decoded.tokenName) {
2103
- const restrictions = protection.protectionFn(decoded.params);
2104
- if (restrictions === false)
2105
- return false;
2106
- return restrictions ?? {};
2107
- }
2108
- }
2109
- return false;
2110
- };
2111
- const applyRestrictions = (options) => {
2112
- const restrictions = getReadRestrictions();
2113
- if (restrictions === false)
2114
- return false;
2115
- if (!restrictions || Object.keys(restrictions).length === 0) {
2116
- return options || {};
2117
- }
2118
- const where = { ...options?.where || {}, ...restrictions };
2119
- return { ...options, where };
2120
- };
2222
+ const restrictions = this.getScopeRestrictions(adapters);
2223
+ const isDenied = this.isScopeDenied(adapters);
2121
2224
  const findRows = async (options) => {
2122
- if (adapters.dataStorage)
2225
+ if (adapters.dataStorage) {
2226
+ if (restrictions) {
2227
+ const scopedStore = new ScopedStore(adapters.dataStorage.getStore(viewName), restrictions, false);
2228
+ return scopedStore.find(options);
2229
+ }
2123
2230
  return adapters.dataStorage.getStore(viewName).find(options);
2231
+ }
2124
2232
  if (adapters.streamingCache)
2125
2233
  return adapters.streamingCache.getStore(viewName).find(options);
2126
2234
  if (adapters.queryWire)
@@ -2134,18 +2242,15 @@ class ArcAggregateElement extends ArcContextElement {
2134
2242
  };
2135
2243
  return {
2136
2244
  find: async (options, mapper) => {
2137
- const restricted = applyRestrictions(options);
2138
- if (restricted === false)
2245
+ if (isDenied)
2139
2246
  return [];
2140
- const rows = await findRows(restricted);
2247
+ const rows = await findRows(options || {});
2141
2248
  return mapper ? rows.map((row) => new mapper(row, adapters)) : rows.map(deserializeRow);
2142
2249
  },
2143
2250
  findOne: async (where, mapper) => {
2144
- const restrictions = getReadRestrictions();
2145
- if (restrictions === false)
2251
+ if (isDenied)
2146
2252
  return;
2147
- const mergedWhere = restrictions && Object.keys(restrictions).length > 0 ? { ...where, ...restrictions } : where;
2148
- const rows = await findRows({ where: mergedWhere });
2253
+ const rows = await findRows({ where: where || {} });
2149
2254
  const row = rows[0];
2150
2255
  if (!row)
2151
2256
  return;
@@ -2153,35 +2258,22 @@ class ArcAggregateElement extends ArcContextElement {
2153
2258
  }
2154
2259
  };
2155
2260
  }
2156
- buildUnrestrictedQuery(adapters) {
2157
- const viewName = this.name;
2158
- const schema = this.schema;
2159
- const deserializeRow = (row) => {
2160
- const { _id, ...rest } = row;
2161
- return { _id, ...schema.deserialize(rest) };
2162
- };
2163
- const findRows = async (options) => {
2164
- if (adapters.dataStorage)
2165
- return adapters.dataStorage.getStore(viewName).find(options);
2166
- if (adapters.streamingCache)
2167
- return adapters.streamingCache.getStore(viewName).find(options);
2168
- if (adapters.queryWire)
2169
- return adapters.queryWire.query(viewName, options);
2170
- return [];
2171
- };
2172
- return {
2173
- find: async (options, mapper) => {
2174
- const rows = await findRows(options || {});
2175
- return mapper ? rows.map((row) => new mapper(row, adapters)) : rows.map(deserializeRow);
2176
- },
2177
- findOne: async (where, mapper) => {
2178
- const rows = await findRows({ where });
2179
- const row = rows[0];
2180
- if (!row)
2181
- return;
2182
- return mapper ? new mapper(row, adapters) : deserializeRow(row);
2261
+ isScopeDenied(adapters) {
2262
+ const protections = this._protections;
2263
+ if (protections.length === 0)
2264
+ return false;
2265
+ if (!adapters.authAdapter)
2266
+ return true;
2267
+ const decoded = adapters.authAdapter.getDecoded();
2268
+ if (!decoded)
2269
+ return true;
2270
+ for (const protection of protections) {
2271
+ if (protection.token.name === decoded.tokenName) {
2272
+ const result = protection.protectionFn(decoded.params);
2273
+ return result === false;
2183
2274
  }
2184
- };
2275
+ }
2276
+ return true;
2185
2277
  }
2186
2278
  getAuth(adapters) {
2187
2279
  if (adapters.authAdapter) {
@@ -2190,12 +2282,33 @@ class ArcAggregateElement extends ArcContextElement {
2190
2282
  }
2191
2283
  return { params: {}, tokenName: "" };
2192
2284
  }
2285
+ getScopeRestrictions(adapters) {
2286
+ const protections = this._protections;
2287
+ if (protections.length === 0)
2288
+ return null;
2289
+ if (!adapters.authAdapter)
2290
+ return null;
2291
+ const decoded = adapters.authAdapter.getDecoded();
2292
+ if (!decoded)
2293
+ return null;
2294
+ for (const protection of protections) {
2295
+ if (protection.token.name === decoded.tokenName) {
2296
+ const result = protection.protectionFn(decoded.params);
2297
+ if (result === false)
2298
+ return null;
2299
+ return result ?? null;
2300
+ }
2301
+ }
2302
+ console.log(`[Scope:${this.name}] no matching protection`);
2303
+ return null;
2304
+ }
2193
2305
  mutateContext(adapters) {
2194
2306
  const events = this._events;
2195
- const privateQuery = this.buildUnrestrictedQuery(adapters);
2307
+ const privateQuery = this.buildPrivateQuery(adapters);
2196
2308
  const auth = this.getAuth(adapters);
2197
2309
  const aggregateName = this.name;
2198
- const buildMutateMethodCtx = () => {
2310
+ const scopeRestrictions = this.getScopeRestrictions(adapters);
2311
+ const buildMutateMethodCtx = (method) => {
2199
2312
  const ctx = {};
2200
2313
  for (const entry of events) {
2201
2314
  const arcEvent = entry.event;
@@ -2206,11 +2319,21 @@ class ArcAggregateElement extends ArcContextElement {
2206
2319
  if (!adapters.eventPublisher) {
2207
2320
  throw new Error(`Cannot emit event "${arcEvent.name}": no eventPublisher adapter available`);
2208
2321
  }
2322
+ if (scopeRestrictions) {
2323
+ for (const [key, value] of Object.entries(scopeRestrictions)) {
2324
+ if (key in payload && payload[key] !== value) {
2325
+ throw new Error(`Scope violation: event "${arcEvent.name}" field "${key}" must be "${value}", got "${payload[key]}"`);
2326
+ }
2327
+ }
2328
+ }
2329
+ const decoded = adapters.authAdapter?.getDecoded() ?? null;
2330
+ const authContext = decoded ? { tokenName: decoded.tokenName, params: decoded.params } : null;
2209
2331
  const eventInstance = {
2210
2332
  type: `${aggregateName}.${arcEvent.name}`,
2211
2333
  payload,
2212
2334
  id: `${Date.now()}_${Math.random().toString(36).slice(2)}`,
2213
- createdAt: new Date
2335
+ createdAt: new Date,
2336
+ authContext
2214
2337
  };
2215
2338
  await adapters.eventPublisher.publish(eventInstance);
2216
2339
  }
@@ -2218,6 +2341,11 @@ class ArcAggregateElement extends ArcContextElement {
2218
2341
  }
2219
2342
  ctx.$auth = auth;
2220
2343
  ctx.$query = privateQuery;
2344
+ if (method.queryElements?.length || method.mutationElements?.length) {
2345
+ const elementCtx = buildElementContext(method.queryElements ?? [], method.mutationElements ?? [], adapters);
2346
+ ctx.query = elementCtx.query;
2347
+ ctx.mutate = elementCtx.mutate;
2348
+ }
2221
2349
  return ctx;
2222
2350
  };
2223
2351
  const result = {};
@@ -2233,7 +2361,7 @@ class ArcAggregateElement extends ArcContextElement {
2233
2361
  } : undefined;
2234
2362
  return adapters.commandWire.executeCommand(`${aggregateName}.${method.name}`, params, wireAuth);
2235
2363
  }
2236
- const ctx = buildMutateMethodCtx();
2364
+ const ctx = buildMutateMethodCtx(method);
2237
2365
  return method.handler(ctx, params);
2238
2366
  };
2239
2367
  }
@@ -2461,9 +2589,10 @@ class ArcListener extends ArcContextElement {
2461
2589
  async handleEvent(event2, adapters) {
2462
2590
  if (!this.data.handler)
2463
2591
  return;
2464
- const context2 = this.#fn.buildContext(adapters);
2465
- if (adapters.authAdapter) {
2466
- const decoded = adapters.authAdapter.getDecoded();
2592
+ const scopedAdapters = this.buildScopedAdapters(event2, adapters);
2593
+ const context2 = this.#fn.buildContext(scopedAdapters);
2594
+ if (scopedAdapters.authAdapter) {
2595
+ const decoded = scopedAdapters.authAdapter.getDecoded();
2467
2596
  if (decoded) {
2468
2597
  context2.$auth = {
2469
2598
  params: decoded.params,
@@ -2479,6 +2608,21 @@ class ArcListener extends ArcContextElement {
2479
2608
  await this.data.handler(context2, event2);
2480
2609
  }
2481
2610
  }
2611
+ buildScopedAdapters(event2, adapters) {
2612
+ if (adapters.authAdapter?.isAuthenticated()) {
2613
+ return adapters;
2614
+ }
2615
+ const authContext = event2.authContext;
2616
+ if (!authContext) {
2617
+ return adapters;
2618
+ }
2619
+ const scopedAuth = new AuthAdapter;
2620
+ scopedAuth.setDecoded({
2621
+ tokenName: authContext.tokenName,
2622
+ params: authContext.params
2623
+ });
2624
+ return { ...adapters, authAdapter: scopedAuth };
2625
+ }
2482
2626
  destroy() {
2483
2627
  for (const unsubscribe of this.unsubscribers) {
2484
2628
  unsubscribe();
@@ -2739,74 +2883,63 @@ class ArcView extends ArcContextElement {
2739
2883
  queryContext(adapters) {
2740
2884
  const viewName = this.data.name;
2741
2885
  const protections = this.data.protections || [];
2742
- const getReadRestrictions = () => {
2743
- if (protections.length === 0)
2744
- return null;
2745
- if (!adapters.authAdapter) {
2746
- return false;
2747
- }
2748
- const decoded = adapters.authAdapter.getDecoded();
2749
- if (!decoded) {
2750
- return false;
2751
- }
2752
- for (const protection of protections) {
2753
- if (protection.token.name === decoded.tokenName) {
2754
- const restrictions = protection.protectionFn(decoded.params);
2755
- if (restrictions === false)
2756
- return false;
2757
- return restrictions ?? {};
2758
- }
2759
- }
2760
- return false;
2761
- };
2762
- const applyRestrictions = (options) => {
2763
- const restrictions = getReadRestrictions();
2764
- if (restrictions === false)
2765
- return false;
2766
- if (!restrictions || Object.keys(restrictions).length === 0) {
2767
- return options || {};
2768
- }
2769
- const where = { ...options?.where || {}, ...restrictions };
2770
- return { ...options, where };
2771
- };
2886
+ const { restrictions, denied } = this.resolveProtection(protections, adapters);
2772
2887
  return {
2773
2888
  find: async (options) => {
2774
- const restrictedOptions = applyRestrictions(options);
2775
- if (restrictedOptions === false) {
2889
+ if (denied)
2776
2890
  return [];
2777
- }
2778
2891
  if (adapters.dataStorage) {
2779
- const store = adapters.dataStorage.getStore(viewName);
2780
- return store.find(restrictedOptions);
2892
+ if (restrictions) {
2893
+ const scopedStore = new ScopedStore(adapters.dataStorage.getStore(viewName), restrictions, false);
2894
+ return scopedStore.find(options);
2895
+ }
2896
+ return adapters.dataStorage.getStore(viewName).find(options);
2781
2897
  }
2782
2898
  if (adapters.queryWire) {
2783
- return adapters.queryWire.query(viewName, restrictedOptions);
2899
+ const where = restrictions ? { ...options?.where || {}, ...restrictions } : options?.where;
2900
+ return adapters.queryWire.query(viewName, { ...options, where });
2784
2901
  }
2785
- console.warn(`View "${viewName}" query: no dataStorage or queryWire available`);
2786
2902
  return [];
2787
2903
  },
2788
2904
  findOne: async (where) => {
2789
- const restrictions = getReadRestrictions();
2790
- if (restrictions === false) {
2905
+ if (denied)
2791
2906
  return;
2792
- }
2793
- const mergedWhere = restrictions && Object.keys(restrictions).length > 0 ? { ...where, ...restrictions } : where;
2794
2907
  if (adapters.dataStorage) {
2795
- const store = adapters.dataStorage.getStore(viewName);
2796
- const results = await store.find({ where: mergedWhere });
2908
+ if (restrictions) {
2909
+ const scopedStore = new ScopedStore(adapters.dataStorage.getStore(viewName), restrictions, false);
2910
+ const results2 = await scopedStore.find({ where: where || {} });
2911
+ return results2[0];
2912
+ }
2913
+ const results = await adapters.dataStorage.getStore(viewName).find({ where });
2797
2914
  return results[0];
2798
2915
  }
2799
2916
  if (adapters.queryWire) {
2800
- const results = await adapters.queryWire.query(viewName, {
2801
- where: mergedWhere
2802
- });
2917
+ const mergedWhere = restrictions ? { ...where, ...restrictions } : where;
2918
+ const results = await adapters.queryWire.query(viewName, { where: mergedWhere });
2803
2919
  return results[0];
2804
2920
  }
2805
- console.warn(`View "${viewName}" query: no dataStorage or queryWire available`);
2806
2921
  return;
2807
2922
  }
2808
2923
  };
2809
2924
  }
2925
+ resolveProtection(protections, adapters) {
2926
+ if (protections.length === 0)
2927
+ return { restrictions: null, denied: false };
2928
+ if (!adapters.authAdapter)
2929
+ return { restrictions: null, denied: true };
2930
+ const decoded = adapters.authAdapter.getDecoded();
2931
+ if (!decoded)
2932
+ return { restrictions: null, denied: true };
2933
+ for (const protection of protections) {
2934
+ if (protection.token.name === decoded.tokenName) {
2935
+ const result = protection.protectionFn(decoded.params);
2936
+ if (result === false)
2937
+ return { restrictions: null, denied: true };
2938
+ return { restrictions: result ?? null, denied: false };
2939
+ }
2940
+ }
2941
+ return { restrictions: null, denied: true };
2942
+ }
2810
2943
  getHandlers() {
2811
2944
  return this.data.handler;
2812
2945
  }
@@ -2925,7 +3058,8 @@ class ArcView extends ArcContextElement {
2925
3058
  type: storedEvent.type,
2926
3059
  payload,
2927
3060
  id: storedEvent._id,
2928
- createdAt: new Date(storedEvent.createdAt)
3061
+ createdAt: new Date(storedEvent.createdAt),
3062
+ authContext: storedEvent.authContext ? typeof storedEvent.authContext === "string" ? JSON.parse(storedEvent.authContext) : storedEvent.authContext : null
2929
3063
  };
2930
3064
  await handler(ctx, eventInstance);
2931
3065
  }
@@ -2943,13 +3077,6 @@ function view(name, id2, schema) {
2943
3077
  protections: []
2944
3078
  });
2945
3079
  }
2946
- // src/data-storage/data-storage.abstract.ts
2947
- class DataStorage {
2948
- async commitChanges(changes) {
2949
- await Promise.all(changes.map(({ store, changes: changes2 }) => this.getStore(store).applyChanges(changes2)));
2950
- }
2951
- }
2952
-
2953
3080
  // src/data-storage/store-state-fork.ts
2954
3081
  import { apply } from "mutative";
2955
3082
 
@@ -4643,7 +4770,8 @@ class StreamingEventPublisher {
4643
4770
  localId: event3.id,
4644
4771
  type: event3.type,
4645
4772
  payload: event3.payload,
4646
- createdAt: event3.createdAt.toISOString()
4773
+ createdAt: event3.createdAt.toISOString(),
4774
+ authContext: event3.authContext ?? null
4647
4775
  }
4648
4776
  ]);
4649
4777
  }
@@ -5091,6 +5219,7 @@ export {
5091
5219
  deepMerge,
5092
5220
  date,
5093
5221
  customId,
5222
+ createScopedDataStorage,
5094
5223
  contextMerge,
5095
5224
  context,
5096
5225
  command,
@@ -5113,7 +5242,9 @@ export {
5113
5242
  StoreState,
5114
5243
  SecuredStoreState,
5115
5244
  SecuredDataStorage,
5245
+ ScopedStore,
5116
5246
  ScopedModel,
5247
+ ScopedDataStorage,
5117
5248
  QueryWire,
5118
5249
  ObservableDataStorage,
5119
5250
  Model,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arcote.tech/arc",
3
3
  "type": "module",
4
- "version": "0.5.1",
4
+ "version": "0.5.5",
5
5
  "private": false,
6
6
  "author": "Przemysław Krasiński [arcote.tech]",
7
7
  "description": "Arc framework core rewrite with improved event emission and type safety",