@agent-native/core 0.145.2 → 0.145.3

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.
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- ok?: undefined;
30
29
  error: string;
30
+ ok?: undefined;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
@@ -13,13 +13,13 @@
13
13
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
14
14
  count: number;
15
15
  updated?: undefined;
16
- ok?: undefined;
17
16
  error?: undefined;
17
+ ok?: undefined;
18
18
  } | {
19
19
  count?: undefined;
20
20
  updated: number;
21
- ok?: undefined;
22
21
  error?: undefined;
22
+ ok?: undefined;
23
23
  } | {
24
24
  count?: undefined;
25
25
  updated?: undefined;
@@ -15,6 +15,6 @@ export declare function createProgressHandler(): import("h3").EventHandlerWithFe
15
15
  error: string;
16
16
  ok?: undefined;
17
17
  } | {
18
- error?: undefined;
19
18
  ok: boolean;
19
+ error?: undefined;
20
20
  }>>;
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
48
48
  }>;
49
49
  /** DELETE /_agent-native/resources/:id — delete a resource */
50
50
  export declare function handleDeleteResource(event: any): Promise<{
51
- ok?: undefined;
52
51
  error: string;
52
+ ok?: undefined;
53
53
  } | {
54
54
  error?: undefined;
55
55
  ok: boolean;
@@ -114,6 +114,15 @@ export interface AppSyncStateOptions {
114
114
  * Postgres only; ignored on SQLite.
115
115
  */
116
116
  dbAssignedVersions?: boolean;
117
+ /**
118
+ * TTL for a cached ALLOW decision. Defaults to 30s.
119
+ *
120
+ * `invalidateCollabAccessCache` only reaches the in-process default instance,
121
+ * so a gateway holding per-app instances cannot be told a share was revoked
122
+ * and serves its cached ALLOW until this lapses. Shorten it to bound that
123
+ * window, at the cost of more `can-see` round-trips per shared resource.
124
+ */
125
+ accessAllowTtlMs?: number;
117
126
  }
118
127
  /**
119
128
  * Per-app change-tracking state and read path. One instance per deployed app.
@@ -180,7 +189,9 @@ export declare class AppSyncState {
180
189
  private readonly accessInFlight;
181
190
  /** Per-resource generation bumped when shares/visibility change. */
182
191
  private readonly accessInvalidationEpoch;
192
+ private readonly accessAllowTtlMs;
183
193
  constructor(options?: AppSyncStateOptions);
194
+ private accessCacheTtl;
184
195
  /**
185
196
  * Durable-event id. Deterministic (hash of logical identity + `dedupeKey`,
186
197
  * excluding the per-instance `version`) when the instance opts in AND a
@@ -153,9 +153,6 @@ function accessCacheKey(userEmail, orgId, resourceType, resourceId) {
153
153
  function accessResourceKey(resourceType, resourceId) {
154
154
  return `${resourceType}|${resourceId}`;
155
155
  }
156
- function accessCacheTtl(allowed) {
157
- return allowed ? ACCESS_CACHE_TTL_MS : ACCESS_CACHE_DENY_TTL_MS;
158
- }
159
156
  function extensionTargetKey(target) {
160
157
  if (target.owner)
161
158
  return `owner:${target.owner}`;
@@ -291,14 +288,19 @@ export class AppSyncState {
291
288
  accessInFlight = new Set();
292
289
  /** Per-resource generation bumped when shares/visibility change. */
293
290
  accessInvalidationEpoch = new Map();
291
+ accessAllowTtlMs;
294
292
  constructor(options = {}) {
295
293
  this.getDb = options.getDb ?? getDbExec;
296
294
  this.isPg = options.isPostgres ?? isPostgres;
297
295
  this.resolveAccessFn = options.resolveAccess ?? defaultResolveAccess;
298
296
  this.deterministicEventIds = options.deterministicEventIds ?? false;
299
297
  this.dbAssignedVersions = options.dbAssignedVersions ?? false;
298
+ this.accessAllowTtlMs = options.accessAllowTtlMs ?? ACCESS_CACHE_TTL_MS;
300
299
  this.pollEmitter.setMaxListeners(0);
301
300
  }
301
+ accessCacheTtl(allowed) {
302
+ return allowed ? this.accessAllowTtlMs : ACCESS_CACHE_DENY_TTL_MS;
303
+ }
302
304
  /**
303
305
  * Durable-event id. Deterministic (hash of logical identity + `dedupeKey`,
304
306
  * excluding the per-instance `version`) when the instance opts in AND a
@@ -675,7 +677,8 @@ export class AppSyncState {
675
677
  const key = accessCacheKey(userEmail, orgId, event.resourceType, event.resourceId);
676
678
  const cached = this.accessCache.get(key);
677
679
  const now = Date.now();
678
- if (cached && now - cached.checkedAt < accessCacheTtl(cached.allowed)) {
680
+ if (cached &&
681
+ now - cached.checkedAt < this.accessCacheTtl(cached.allowed)) {
679
682
  // Fresh, non-expired cache hit → trust the cached decision.
680
683
  return cached.allowed ? "visible" : "hidden";
681
684
  }
@@ -26,8 +26,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
26
26
  expiresAt?: undefined;
27
27
  ttlSeconds?: undefined;
28
28
  } | {
29
- error?: undefined;
30
29
  token: string;
31
30
  expiresAt: string;
32
31
  ttlSeconds: number;
32
+ error?: undefined;
33
33
  }>>;
@@ -28,6 +28,13 @@ import { signRealtimeSubscribeToken } from "./short-lived-token.js";
28
28
  export const REALTIME_HMAC_SECRET_ENV = "AGENT_NATIVE_REALTIME_HMAC_SECRET";
29
29
  /** Short TTL — validated at connect; the gateway rotates over the stream. */
30
30
  const REALTIME_TOKEN_TTL_SECONDS = 600;
31
+ /**
32
+ * Ceiling on how long one mint can be extended by gateway rotation before the
33
+ * client must come back through this session-gated endpoint. Without it a single
34
+ * mint streams forever and logout, session expiry, user deletion and org removal
35
+ * never reach an open stream.
36
+ */
37
+ const REALTIME_SESSION_MAX_SECONDS = 15 * 60;
31
38
  export function getRealtimeSigningSecret() {
32
39
  return process.env[REALTIME_HMAC_SECRET_ENV]?.trim() || undefined;
33
40
  }
@@ -76,6 +83,7 @@ export function createRealtimeTokenHandler() {
76
83
  owner: session.email,
77
84
  orgId: requestContext.orgId,
78
85
  ttlSeconds: REALTIME_TOKEN_TTL_SECONDS,
86
+ absExp: Math.floor(Date.now() / 1000) + REALTIME_SESSION_MAX_SECONDS,
79
87
  }, secret);
80
88
  const expiresAt = new Date(Date.now() + REALTIME_TOKEN_TTL_SECONDS * 1000).toISOString();
81
89
  return { token, expiresAt, ttlSeconds: REALTIME_TOKEN_TTL_SECONDS };
@@ -82,6 +82,13 @@ export interface RealtimeSubscribeClaims {
82
82
  orgId?: string;
83
83
  /** Override default TTL (seconds). */
84
84
  ttlSeconds?: number;
85
+ /**
86
+ * Absolute unix-seconds ceiling, independent of `exp`. The gateway re-signs a
87
+ * stream's token every few minutes without consulting the app, so `exp` alone
88
+ * lets one mint be extended forever and a revoked session keeps streaming.
89
+ * Rotation copies this verbatim and never extends it.
90
+ */
91
+ absExp?: number;
85
92
  }
86
93
  /**
87
94
  * Result of {@link verifyRealtimeSubscribeToken}. On success it returns the
@@ -93,6 +100,7 @@ export type RealtimeVerifyResult = {
93
100
  owner?: string;
94
101
  orgId?: string;
95
102
  exp: number;
103
+ absExp?: number;
96
104
  } | {
97
105
  ok: false;
98
106
  reason: string;
@@ -182,6 +182,8 @@ export function signRealtimeSubscribeToken(claims, key) {
182
182
  payload.owner = claims.owner;
183
183
  if (claims.orgId)
184
184
  payload.orgId = claims.orgId;
185
+ if (claims.absExp)
186
+ payload.absExp = claims.absExp;
185
187
  const payloadStr = base64UrlEncode(JSON.stringify(payload));
186
188
  return `${payloadStr}.${hmacB64(payloadStr, key)}`;
187
189
  }
@@ -218,6 +220,15 @@ export function verifyRealtimeSubscribeToken(token, expected) {
218
220
  if (claims.exp * 1000 < Date.now()) {
219
221
  return { ok: false, reason: "expired" };
220
222
  }
223
+ if (claims.absExp !== undefined) {
224
+ if (typeof claims.absExp !== "number") {
225
+ return { ok: false, reason: "bad_payload" };
226
+ }
227
+ // `<=`, not `<`: a ceiling is not valid at the instant it is reached.
228
+ if (claims.absExp * 1000 <= Date.now()) {
229
+ return { ok: false, reason: "session_expired" };
230
+ }
231
+ }
221
232
  if (claims.projectId !== expected.projectId) {
222
233
  return { ok: false, reason: "wrong_project" };
223
234
  }
@@ -227,6 +238,7 @@ export function verifyRealtimeSubscribeToken(token, expected) {
227
238
  owner: claims.owner,
228
239
  orgId: claims.orgId,
229
240
  exp: claims.exp,
241
+ absExp: claims.absExp,
230
242
  };
231
243
  }
232
244
  // ── Gateway access-check tokens ──────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.145.2",
3
+ "version": "0.145.3",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {