@arcote.tech/arc 0.5.2 → 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";
@@ -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
@@ -77,11 +77,17 @@ export declare class ArcListener<const Data extends ArcListenerData> extends Arc
77
77
  init(environment: ArcEnvironment, adapters: ModelAdapters): Promise<void>;
78
78
  private handleEvent;
79
79
  /**
80
- * Build scoped adapters from event payload.
80
+ * Build scoped adapters for the listener handler.
81
81
  *
82
- * Looks at declared query/mutate elements if any have protections,
83
- * extracts the token name and creates an AuthAdapter with event payload
84
- * as decoded params. This allows aggregate restrictions to apply.
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.
85
91
  */
86
92
  private buildScopedAdapters;
87
93
  destroy(): void;
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,
@@ -1775,11 +1781,14 @@ class ArcEvent extends ArcContextElement {
1775
1781
  if (!adapters.eventPublisher) {
1776
1782
  throw new Error(`Event "${this.data.name}" cannot be emitted: no eventPublisher adapter available`);
1777
1783
  }
1784
+ const decoded = adapters.authAdapter?.getDecoded() ?? null;
1785
+ const authContext = decoded ? { tokenName: decoded.tokenName, params: decoded.params } : null;
1778
1786
  const event = {
1779
1787
  type: this.data.name,
1780
1788
  payload,
1781
1789
  id: this.eventId.generate(),
1782
- createdAt: new Date
1790
+ createdAt: new Date,
1791
+ authContext
1783
1792
  };
1784
1793
  await adapters.eventPublisher.publish(event);
1785
1794
  }
@@ -1997,11 +2006,14 @@ class AggregateBase {
1997
2006
  if (!adapters.eventPublisher) {
1998
2007
  throw new Error(`Cannot emit event "${arcEvent.name}": no eventPublisher adapter available`);
1999
2008
  }
2009
+ const decoded = adapters.authAdapter?.getDecoded() ?? null;
2010
+ const authContext = decoded ? { tokenName: decoded.tokenName, params: decoded.params } : null;
2000
2011
  const eventInstance = {
2001
2012
  type: arcEvent.name,
2002
2013
  payload,
2003
2014
  id: `${Date.now()}_${Math.random().toString(36).slice(2)}`,
2004
- createdAt: new Date
2015
+ createdAt: new Date,
2016
+ authContext
2005
2017
  };
2006
2018
  await adapters.eventPublisher.publish(eventInstance);
2007
2019
  }
@@ -2314,11 +2326,14 @@ class ArcAggregateElement extends ArcContextElement {
2314
2326
  }
2315
2327
  }
2316
2328
  }
2329
+ const decoded = adapters.authAdapter?.getDecoded() ?? null;
2330
+ const authContext = decoded ? { tokenName: decoded.tokenName, params: decoded.params } : null;
2317
2331
  const eventInstance = {
2318
2332
  type: `${aggregateName}.${arcEvent.name}`,
2319
2333
  payload,
2320
2334
  id: `${Date.now()}_${Math.random().toString(36).slice(2)}`,
2321
- createdAt: new Date
2335
+ createdAt: new Date,
2336
+ authContext
2322
2337
  };
2323
2338
  await adapters.eventPublisher.publish(eventInstance);
2324
2339
  }
@@ -2597,31 +2612,15 @@ class ArcListener extends ArcContextElement {
2597
2612
  if (adapters.authAdapter?.isAuthenticated()) {
2598
2613
  return adapters;
2599
2614
  }
2600
- const allElements = [
2601
- ...this.data.queryElements,
2602
- ...this.data.mutationElements
2603
- ];
2604
- let tokenName = null;
2605
- for (const element of allElements) {
2606
- const protections = element.__aggregateProtections ?? element.data?.protections;
2607
- if (protections?.length > 0) {
2608
- tokenName = protections[0].token.name;
2609
- break;
2610
- }
2611
- }
2612
- if (!tokenName || !event2.payload) {
2615
+ const authContext = event2.authContext;
2616
+ if (!authContext) {
2613
2617
  return adapters;
2614
2618
  }
2615
2619
  const scopedAuth = new AuthAdapter;
2616
- scopedAuth.scopes = new Map([
2617
- ["default", {
2618
- raw: "",
2619
- decoded: {
2620
- tokenName,
2621
- params: event2.payload
2622
- }
2623
- }]
2624
- ]);
2620
+ scopedAuth.setDecoded({
2621
+ tokenName: authContext.tokenName,
2622
+ params: authContext.params
2623
+ });
2625
2624
  return { ...adapters, authAdapter: scopedAuth };
2626
2625
  }
2627
2626
  destroy() {
@@ -3059,7 +3058,8 @@ class ArcView extends ArcContextElement {
3059
3058
  type: storedEvent.type,
3060
3059
  payload,
3061
3060
  id: storedEvent._id,
3062
- 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
3063
3063
  };
3064
3064
  await handler(ctx, eventInstance);
3065
3065
  }
@@ -4770,7 +4770,8 @@ class StreamingEventPublisher {
4770
4770
  localId: event3.id,
4771
4771
  type: event3.type,
4772
4772
  payload: event3.payload,
4773
- createdAt: event3.createdAt.toISOString()
4773
+ createdAt: event3.createdAt.toISOString(),
4774
+ authContext: event3.authContext ?? null
4774
4775
  }
4775
4776
  ]);
4776
4777
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arcote.tech/arc",
3
3
  "type": "module",
4
- "version": "0.5.2",
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",