@agent-native/core 0.205.0 → 0.206.0-nightly-20261009011246

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 (115) hide show
  1. package/dist/a2a/client.d.ts +2 -2
  2. package/dist/agent/engine/anthropic-engine.d.ts +1 -1
  3. package/dist/agent/engine/builder-engine.d.ts +1 -1
  4. package/dist/agent/engine/builder-engine.js +23 -8
  5. package/dist/agent/engine/error-telemetry.d.ts +5 -0
  6. package/dist/agent/engine/error-telemetry.js +41 -0
  7. package/dist/agent/engine/registry.d.ts +1 -6
  8. package/dist/agent/engine/registry.js +2 -31
  9. package/dist/agent/model-config.d.ts +26 -14
  10. package/dist/agent/model-config.js +131 -8
  11. package/dist/agent/model-version.d.ts +19 -0
  12. package/dist/agent/model-version.js +153 -0
  13. package/dist/agent/production-agent.d.ts +1 -0
  14. package/dist/agent/production-agent.js +24 -6
  15. package/dist/agent/provider-model-selection.js +6 -4
  16. package/dist/agent/run-manager.js +5 -11
  17. package/dist/agent/thread-data-builder.js +14 -6
  18. package/dist/authorization/check-action.js +8 -4
  19. package/dist/chat-threads/store.js +22 -9
  20. package/dist/client/analytics.d.ts +1 -0
  21. package/dist/client/analytics.js +10 -2
  22. package/dist/client/chat/runtime.js +18 -0
  23. package/dist/client/chat-model-groups.d.ts +3 -0
  24. package/dist/client/error-format.js +10 -7
  25. package/dist/client/onboarding/use-onboarding.d.ts +12 -0
  26. package/dist/client/onboarding/use-onboarding.js +491 -4
  27. package/dist/client/session-replay-privacy.d.ts +8 -0
  28. package/dist/client/session-replay-privacy.js +8 -0
  29. package/dist/client/session-replay.d.ts +1 -0
  30. package/dist/client/session-replay.js +18 -9
  31. package/dist/client/sse-event-processor.js +10 -6
  32. package/dist/client/use-chat-models.d.ts +5 -0
  33. package/dist/client/use-chat-models.js +51 -5
  34. package/dist/client/use-session.d.ts +8 -1
  35. package/dist/client/use-session.js +20 -1
  36. package/dist/collab/struct-routes.d.ts +1 -1
  37. package/dist/deploy/build.js +353 -3
  38. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  39. package/dist/jobs/backfill-run-history-ownership.d.ts +12 -0
  40. package/dist/jobs/backfill-run-history-ownership.js +87 -0
  41. package/dist/jobs/background-automation-runner.js +3 -9
  42. package/dist/jobs/remote-execution.js +3 -9
  43. package/dist/jobs/run-history-ownership.d.ts +5 -0
  44. package/dist/jobs/run-history-ownership.js +16 -0
  45. package/dist/jobs/run-now.js +2 -3
  46. package/dist/localization/agent-resources-messages.d.ts +3 -0
  47. package/dist/localization/agent-resources-messages.js +33 -0
  48. package/dist/mcp/actions/create-org-service-token.js +5 -7
  49. package/dist/mcp/build-server.d.ts +23 -18
  50. package/dist/mcp/build-server.js +220 -212
  51. package/dist/mcp/connect-route.d.ts +11 -2
  52. package/dist/mcp/connect-route.js +34 -24
  53. package/dist/mcp/connect-store.d.ts +9 -13
  54. package/dist/mcp/connect-store.js +11 -24
  55. package/dist/mcp/credential-membership.js +17 -6
  56. package/dist/mcp/oauth-route.d.ts +11 -1
  57. package/dist/mcp/oauth-route.js +28 -6
  58. package/dist/mcp/oauth-token.d.ts +12 -2
  59. package/dist/mcp/oauth-token.js +15 -4
  60. package/dist/mcp/server.js +9 -5
  61. package/dist/notifications/routes.d.ts +2 -2
  62. package/dist/observability/posthog-ai.d.ts +1 -0
  63. package/dist/observability/posthog-ai.js +20 -8
  64. package/dist/observability/routes.d.ts +6 -6
  65. package/dist/observability/traces.js +61 -39
  66. package/dist/org/app-roles.d.ts +8 -1
  67. package/dist/org/app-roles.js +29 -9
  68. package/dist/org/context.js +24 -8
  69. package/dist/org/index.d.ts +2 -2
  70. package/dist/org/workspace-app-identity.d.ts +1 -0
  71. package/dist/org/workspace-app-identity.js +25 -0
  72. package/dist/progress/routes.d.ts +1 -1
  73. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  74. package/dist/provider-api/actions/provider-api.d.ts +3 -3
  75. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  76. package/dist/scripts/agent-engines/list-agent-engines.js +1 -0
  77. package/dist/server/action-routes.js +8 -1
  78. package/dist/server/agent-chat-plugin.js +1 -0
  79. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  80. package/dist/server/auth.js +33 -17
  81. package/dist/server/bearer-credential-refusal.d.ts +23 -0
  82. package/dist/server/bearer-credential-refusal.js +67 -0
  83. package/dist/server/better-auth-instance.js +13 -6
  84. package/dist/server/builder-browser.d.ts +2 -0
  85. package/dist/server/builder-browser.js +16 -3
  86. package/dist/server/capture-error.d.ts +1 -0
  87. package/dist/server/capture-error.js +28 -4
  88. package/dist/server/complete-text.js +16 -3
  89. package/dist/server/core-routes-plugin.js +2 -0
  90. package/dist/server/dev-action-bridge.js +7 -1
  91. package/dist/server/google-oauth.js +6 -3
  92. package/dist/server/index.d.ts +1 -1
  93. package/dist/server/index.js +1 -1
  94. package/dist/server/realtime-token.d.ts +1 -1
  95. package/dist/server/recap-image-route.js +4 -0
  96. package/dist/server/request-context.d.ts +16 -0
  97. package/dist/server/request-context.js +17 -0
  98. package/dist/shared/agent-chat-context.d.ts +1 -0
  99. package/dist/shared/agent-chat-context.js +26 -0
  100. package/dist/shared/index.d.ts +1 -1
  101. package/dist/shared/index.js +1 -1
  102. package/dist/shared/reasoning-effort.js +10 -1
  103. package/dist/shared/ssr-session-bootstrap.d.ts +7 -0
  104. package/dist/shared/ssr-session-bootstrap.js +21 -0
  105. package/dist/sharing/access.js +86 -5
  106. package/dist/templates/builder-code-starter/server/routes/index.get.ts +2 -0
  107. package/dist/triggers/routes.js +2 -1
  108. package/docs/content/automation-connectors.mdx +5 -3
  109. package/docs/content/automations.mdx +78 -49
  110. package/docs/content/observability.mdx +16 -5
  111. package/docs/content/organizations-teams-permissions.mdx +44 -2
  112. package/docs/content/recurring-jobs.mdx +4 -8
  113. package/docs/content/template-forms.mdx +6 -0
  114. package/package.json +5 -4
  115. package/src/templates/builder-code-starter/server/routes/index.get.ts +2 -0
@@ -1,4 +1,5 @@
1
1
  import { credentialStateTrackingProperties } from "../agent/engine/credential-state.js";
2
+ import { runErrorTelemetryProperties } from "../agent/engine/error-telemetry.js";
2
3
  import { isToolDoneFailure } from "../agent/tool-done-error.js";
3
4
  import { captureError } from "../server/capture-error.js";
4
5
  import { getRequestContext, getRequestOrgId, } from "../server/request-context.js";
@@ -9,6 +10,28 @@ export { redactSensitiveFields } from "./trace-redaction.js";
9
10
  import { recordAgentToolCall, recordGenAiChat, recordTraceWriteFailure, } from "./metrics.js";
10
11
  import { endAgentSpan, startAgentSpan, withAgentSpanContext, } from "./tracing.js";
11
12
  import { trackingIdentityProperties } from "./tracking-identity.js";
13
+ function endRunTelemetrySpan(span, { errorCode, ...result }) {
14
+ if (result.status !== "error") {
15
+ endAgentSpan(span, result);
16
+ return;
17
+ }
18
+ const code = errorCode ?? result.attributes?.["agent.terminal_code"];
19
+ const { error_code, error_cause } = runErrorTelemetryProperties(typeof code === "string" ? code : undefined, result.errorMessage);
20
+ endAgentSpan(span, {
21
+ ...result,
22
+ errorMessage: `Agent run failed (${error_code})`,
23
+ attributes: {
24
+ ...result.attributes,
25
+ "agent.error_code": error_code,
26
+ "agent.error_cause": error_cause,
27
+ },
28
+ });
29
+ }
30
+ function endToolTelemetrySpan(span, result) {
31
+ endAgentSpan(span, result.status === "error"
32
+ ? { ...result, errorMessage: "Tool call failed" }
33
+ : result);
34
+ }
12
35
  function spanId() {
13
36
  return `span-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
14
37
  }
@@ -136,6 +159,8 @@ function aiTraceMetadataProperties(metadata) {
136
159
  return {};
137
160
  const properties = {};
138
161
  for (const [key, value] of Object.entries(metadata)) {
162
+ if (key === "error_message" || key === "errorMessage")
163
+ continue;
139
164
  if (value === undefined || value === null)
140
165
  continue;
141
166
  const scalar = typeof value === "string"
@@ -234,7 +259,7 @@ function emitLlmGenerationTrackingEvent(args) {
234
259
  $ai_is_error: args.status === "error",
235
260
  $ai_error: resolveAiError(args.status === "error", toAiErrorDetail(error, {
236
261
  state: args.terminalOutcome?.state,
237
- code: terminalCode,
262
+ code: terminalCode ?? args.errorCode,
238
263
  retryable: terminalRetryable,
239
264
  })),
240
265
  $ai_error_type: args.status === "error" ? (terminalCode ?? "llm_error") : undefined,
@@ -267,8 +292,6 @@ function emitLlmGenerationTrackingEvent(args) {
267
292
  properties.experiment_variant = args.experimentAssignments[0].variantId;
268
293
  }
269
294
  }
270
- if (error)
271
- properties.error_message = error;
272
295
  for (const key of Object.keys(properties)) {
273
296
  if (properties[key] === undefined)
274
297
  delete properties[key];
@@ -435,7 +458,7 @@ export async function instrumentAgentLoop(opts) {
435
458
  return;
436
459
  if (entry.endResult) {
437
460
  entry.ended = true;
438
- endAgentSpan(span, entry.endResult);
461
+ endRunTelemetrySpan(span, entry.endResult);
439
462
  }
440
463
  else {
441
464
  entry.span = span;
@@ -458,7 +481,7 @@ export async function instrumentAgentLoop(opts) {
458
481
  return;
459
482
  entry.ended = true;
460
483
  openOtelModelSpans.delete(entry.span);
461
- endAgentSpan(entry.span, safeResult);
484
+ endRunTelemetrySpan(entry.span, safeResult);
462
485
  };
463
486
  const recordPreStreamModelFailure = (start, end, input, errorMessage) => {
464
487
  const tripIndex = modelRoundTrips.length;
@@ -479,7 +502,7 @@ export async function instrumentAgentLoop(opts) {
479
502
  endTime: end,
480
503
  });
481
504
  };
482
- const finishAwaitingOtelModelSpans = (finalErrorMessage = null, markUnresolvedFailed = false) => {
505
+ const finishAwaitingOtelModelSpans = (finalErrorMessage = null, markUnresolvedFailed = false, finalErrorCode) => {
483
506
  for (const tripIndex of modelSpansAwaitingFinalError) {
484
507
  const trip = modelRoundTrips[tripIndex];
485
508
  const modelCallFailed = trip?.stopReason === "error" ||
@@ -500,6 +523,7 @@ export async function instrumentAgentLoop(opts) {
500
523
  finishOtelModelSpan(tripIndex, {
501
524
  status: modelCallFailed ? "error" : "success",
502
525
  errorMessage: modelErrorMessage,
526
+ errorCode: finalErrorCode,
503
527
  attributes: modelSpanAttributes(tripIndex),
504
528
  endTime: modelRoundTrips[tripIndex]?.end,
505
529
  });
@@ -516,6 +540,7 @@ export async function instrumentAgentLoop(opts) {
516
540
  let usage;
517
541
  let runStatus = "success";
518
542
  let errorMessage = null;
543
+ let errorCode;
519
544
  let runMetadata = opts.metadata ?? null;
520
545
  let terminalOutcome;
521
546
  let errorHttpStatus;
@@ -566,6 +591,7 @@ export async function instrumentAgentLoop(opts) {
566
591
  finishAwaitingOtelModelSpans(null, event.type === "clear");
567
592
  runStatus = "success";
568
593
  errorMessage = null;
594
+ errorCode = undefined;
569
595
  cutOffReason = null;
570
596
  }
571
597
  else if (event.type === "auto_continue") {
@@ -579,6 +605,7 @@ export async function instrumentAgentLoop(opts) {
579
605
  else if (event.type === "error") {
580
606
  runStatus = "error";
581
607
  errorMessage = redactCapturedError(event.error);
608
+ errorCode = event.errorCode;
582
609
  }
583
610
  else if (event.type === "tripwire") {
584
611
  runStatus = "error";
@@ -652,7 +679,7 @@ export async function instrumentAgentLoop(opts) {
652
679
  if (!span)
653
680
  return;
654
681
  if (entry.endResult) {
655
- endAgentSpan(span, {
682
+ endToolTelemetrySpan(span, {
656
683
  status: entry.endResult.status,
657
684
  errorMessage: entry.endResult.errorMessage,
658
685
  });
@@ -705,8 +732,7 @@ export async function instrumentAgentLoop(opts) {
705
732
  }
706
733
  else
707
734
  successfulTools++;
708
- // The full text goes to external sinks only when captureToolResults is
709
- // on; the persisted span always keeps at least the signature.
735
+ // Local spans keep the error text; tracking keeps only its class.
710
736
  const toolErrorMessage = isError && config.captureToolResults
711
737
  ? sanitizeToolErrorMessage(event.result)
712
738
  : null;
@@ -726,7 +752,6 @@ export async function instrumentAgentLoop(opts) {
726
752
  : explicitError
727
753
  ? "tool_error"
728
754
  : "legacy_inferred_error",
729
- error_message: toolErrorMessage ?? undefined,
730
755
  });
731
756
  }
732
757
  recordAgentToolCall({
@@ -743,7 +768,7 @@ export async function instrumentAgentLoop(opts) {
743
768
  };
744
769
  if (pending?.otelSpan) {
745
770
  openOtelToolSpans.delete(pending.otelSpan);
746
- endAgentSpan(pending.otelSpan, {
771
+ endToolTelemetrySpan(pending.otelSpan, {
747
772
  status: otelEndResult.status,
748
773
  errorMessage: otelEndResult.errorMessage,
749
774
  attributes: { "gen_ai.tool.name": event.tool },
@@ -839,6 +864,7 @@ export async function instrumentAgentLoop(opts) {
839
864
  ? (err?.message ?? String(err))
840
865
  : classification.errorMessage);
841
866
  errorHttpStatus = httpStatusFromError(err);
867
+ errorCode = typeof err?.errorCode === "string" ? err.errorCode : undefined;
842
868
  const errorMetadata = classification?.metadata ?? null;
843
869
  runMetadata =
844
870
  runMetadata || errorMetadata
@@ -899,12 +925,11 @@ export async function instrumentAgentLoop(opts) {
899
925
  duration_ms: Math.max(0, runEnd - pending.startMs),
900
926
  status: "error",
901
927
  error_class: "interrupted",
902
- error_message: capturedInterruptedMessage ?? undefined,
903
928
  });
904
929
  }
905
930
  if (pending.otelSpan) {
906
931
  openOtelToolSpans.delete(pending.otelSpan);
907
- endAgentSpan(pending.otelSpan, {
932
+ endToolTelemetrySpan(pending.otelSpan, {
908
933
  status: "error",
909
934
  errorMessage: capturedInterruptedMessage,
910
935
  attributes: { "gen_ai.tool.name": pending.toolName },
@@ -1149,6 +1174,7 @@ export async function instrumentAgentLoop(opts) {
1149
1174
  status: generationStatus,
1150
1175
  errorMessage: capturedGenerationError,
1151
1176
  httpStatus: generationStatus === "error" ? errorHttpStatus : HTTP_STATUS_OK,
1177
+ errorCode: generationStatus === "error" ? errorCode : undefined,
1152
1178
  toolCalls: generation.toolSpans.length,
1153
1179
  successfulTools: generation.toolSpans.filter((span) => span.status === "success").length,
1154
1180
  failedTools: generation.toolSpans.filter((span) => span.status === "error").length,
@@ -1196,7 +1222,7 @@ export async function instrumentAgentLoop(opts) {
1196
1222
  code: effectiveTerminalOutcome?.state === "failed" ||
1197
1223
  effectiveTerminalOutcome?.state === "input_required"
1198
1224
  ? effectiveTerminalOutcome.code
1199
- : undefined,
1225
+ : errorCode,
1200
1226
  retryable: effectiveTerminalOutcome?.state === "failed"
1201
1227
  ? effectiveTerminalOutcome.retryable
1202
1228
  : undefined,
@@ -1263,24 +1289,16 @@ export async function instrumentAgentLoop(opts) {
1263
1289
  },
1264
1290
  });
1265
1291
  for (const span of emittedToolSpans) {
1266
- const toolErrorMessage = span.status === "error" &&
1267
- span.errorMessage &&
1268
- config.captureToolResults
1269
- ? sanitizeToolErrorMessage(span.errorMessage)
1270
- : undefined;
1271
1292
  const toolErrorDetail = span.status !== "error"
1272
1293
  ? undefined
1273
- : toolErrorMessage
1274
- ? toAiErrorDetail(toolErrorMessage)
1275
- : !config.captureToolResults
1276
- ? {
1277
- message: "error text withheld: captureToolResults is off for this app",
1278
- }
1279
- : undefined;
1280
- const toolOutputState = config.captureToolResults
1281
- ? (toolErrorMessage ??
1282
- span.metadata?.output)
1283
- : "[tool result withheld: captureToolResults is off for this app]";
1294
+ : toAiErrorDetail(undefined, {
1295
+ code: toolSpanErrorClass.get(span.id) ?? "tool_error",
1296
+ });
1297
+ const toolOutputState = span.status === "error"
1298
+ ? "[tool error message omitted from telemetry]"
1299
+ : config.captureToolResults
1300
+ ? span.metadata?.output
1301
+ : "[tool result withheld: captureToolResults is off for this app]";
1284
1302
  const requestingGeneration = toolSpanRoundTrip.get(span.id);
1285
1303
  emitAiSpanEvent({
1286
1304
  runId,
@@ -1329,6 +1347,10 @@ export async function instrumentAgentLoop(opts) {
1329
1347
  createdAt: runStart,
1330
1348
  };
1331
1349
  writeTraceData(spans, summary, runId, config).catch((error) => reportTraceWriteFailure(runId, "write", error));
1350
+ const runTerminalCode = effectiveTerminalOutcome?.state === "failed" ||
1351
+ effectiveTerminalOutcome?.state === "input_required"
1352
+ ? effectiveTerminalOutcome.code
1353
+ : errorCode;
1332
1354
  try {
1333
1355
  // Metrics are unsampled and independent of trace export.
1334
1356
  for (const [tripIndex, trip] of modelRoundTrips.entries()) {
@@ -1365,6 +1387,7 @@ export async function instrumentAgentLoop(opts) {
1365
1387
  finishOtelModelSpan(interruptedModelRoundTrip, {
1366
1388
  status: "error",
1367
1389
  errorMessage: errorMessage ?? "Model stream interrupted before completion.",
1390
+ errorCode: runTerminalCode,
1368
1391
  attributes: modelSpanAttributes(interruptedModelRoundTrip),
1369
1392
  endTime: runEnd,
1370
1393
  });
@@ -1379,7 +1402,7 @@ export async function instrumentAgentLoop(opts) {
1379
1402
  });
1380
1403
  }
1381
1404
  }
1382
- finishAwaitingOtelModelSpans(errorMessage);
1405
+ finishAwaitingOtelModelSpans(errorMessage, false, runTerminalCode);
1383
1406
  await Promise.all([...pendingOtelModelSpans.values()].map((entry) => entry.spanPromise));
1384
1407
  if (usage && modelRoundTrips.length === 0) {
1385
1408
  const aggregateLlmSpan = await withAgentSpanContext(otelRunSpan, () => startAgentSpan(`chat ${loopOpts.model}`, {
@@ -1387,9 +1410,10 @@ export async function instrumentAgentLoop(opts) {
1387
1410
  "gen_ai.provider.name": engineName,
1388
1411
  "gen_ai.request.model": loopOpts.model,
1389
1412
  }, otelRunSpan));
1390
- endAgentSpan(aggregateLlmSpan, {
1413
+ endRunTelemetrySpan(aggregateLlmSpan, {
1391
1414
  status: runStatus,
1392
1415
  errorMessage: errorMessage === null ? null : redactCapturedString(errorMessage),
1416
+ errorCode: runTerminalCode,
1393
1417
  attributes: {
1394
1418
  "gen_ai.response.model": usage.model,
1395
1419
  ...(usage.usageReported
@@ -1405,20 +1429,21 @@ export async function instrumentAgentLoop(opts) {
1405
1429
  });
1406
1430
  }
1407
1431
  for (const modelSpan of openOtelModelSpans) {
1408
- endAgentSpan(modelSpan, {
1432
+ endRunTelemetrySpan(modelSpan, {
1409
1433
  status: "error",
1410
1434
  errorMessage: "Agent run ended before model_stream completed.",
1435
+ errorCode: runTerminalCode,
1411
1436
  });
1412
1437
  }
1413
1438
  openOtelModelSpans.clear();
1414
1439
  for (const toolSpan of openOtelToolSpans) {
1415
- endAgentSpan(toolSpan, {
1440
+ endToolTelemetrySpan(toolSpan, {
1416
1441
  status: "error",
1417
1442
  errorMessage: "Agent run ended before tool_done.",
1418
1443
  });
1419
1444
  }
1420
1445
  openOtelToolSpans.clear();
1421
- endAgentSpan(otelRunSpan, {
1446
+ endRunTelemetrySpan(otelRunSpan, {
1422
1447
  status: runStatus,
1423
1448
  errorMessage: redactCapturedError(errorMessage),
1424
1449
  attributes: {
@@ -1437,10 +1462,7 @@ export async function instrumentAgentLoop(opts) {
1437
1462
  : undefined,
1438
1463
  "agent.cost_cents_x100": costCentsX100,
1439
1464
  "agent.terminal_state": effectiveTerminalOutcome?.state,
1440
- "agent.terminal_code": effectiveTerminalOutcome?.state === "failed" ||
1441
- effectiveTerminalOutcome?.state === "input_required"
1442
- ? effectiveTerminalOutcome.code
1443
- : undefined,
1465
+ "agent.terminal_code": runTerminalCode,
1444
1466
  },
1445
1467
  });
1446
1468
  // coercion-ok: OTel export must never break the run.
@@ -12,9 +12,12 @@ export type AppRoleLookup<R extends string = string> = {
12
12
  status: "assigned";
13
13
  roles: R[];
14
14
  orgId: string;
15
+ orgRole?: string;
15
16
  } | {
16
17
  status: "unassigned";
17
18
  orgId: string;
19
+ orgRole?: string;
20
+ hasAssignments?: boolean;
18
21
  } | {
19
22
  status: "not-a-member";
20
23
  orgId: string;
@@ -27,6 +30,10 @@ export interface AppRoleCaller {
27
30
  userEmail?: string | null;
28
31
  orgId?: string | null;
29
32
  }
33
+ export interface AppPermissionPolicy<R extends string> {
34
+ unassignedRole?: R;
35
+ allowOrgAdmins?: boolean;
36
+ }
30
37
  export interface AppAuthorizationContext {
31
38
  appId: string;
32
39
  roles: string[];
@@ -44,7 +51,7 @@ export interface AppRoles<R extends string = string, P extends string = string>
44
51
  }
45
52
  export declare function getRegisteredAppRoles(appId: string): AppRolesDescriptor<string, string> | undefined;
46
53
  export declare function listRegisteredAppRoles(): AppRolesDescriptor<string, string>[];
47
- export declare function defineAppRoles<const R extends string, const P extends string = string>(descriptor: AppRolesDescriptor<R, P>): AppRoles<R, P>;
54
+ export declare function defineAppRoles<const R extends string, const P extends string = string>(descriptor: AppRolesDescriptor<R, P>, permissionPolicy?: AppPermissionPolicy<R>): AppRoles<R, P>;
48
55
  export declare function resolveAppRole<R extends string>(descriptor: AppRolesDescriptor<R>, caller?: AppRoleCaller): Promise<AppRoleLookup<R>>;
49
56
  export interface AppMemberRoleRow {
50
57
  email: string;
@@ -18,13 +18,17 @@ function callerIdentity(caller) {
18
18
  orgId: orgId && orgId.trim() ? orgId.trim() : null,
19
19
  };
20
20
  }
21
- export function defineAppRoles(descriptor) {
21
+ export function defineAppRoles(descriptor, permissionPolicy = {}) {
22
22
  const appId = descriptor.appId.trim();
23
23
  if (!appId)
24
24
  throw new Error("defineAppRoles: appId is required");
25
25
  if (!descriptor.roles.length) {
26
26
  throw new Error(`defineAppRoles(${appId}): at least one role is required`);
27
27
  }
28
+ if (permissionPolicy.unassignedRole &&
29
+ !descriptor.roles.includes(permissionPolicy.unassignedRole)) {
30
+ throw new Error(`defineAppRoles(${appId}): unassignedRole is not in roles`);
31
+ }
28
32
  if (descriptor.defaultRole &&
29
33
  !descriptor.roles.includes(descriptor.defaultRole)) {
30
34
  throw new Error(`defineAppRoles(${appId}): defaultRole "${descriptor.defaultRole}" is not in roles`);
@@ -61,11 +65,19 @@ export function defineAppRoles(descriptor) {
61
65
  if (!identity.email || !identity.orgId)
62
66
  throw new ForbiddenError(`Requires ${appId} permission ${permissions.join(" or ")}`);
63
67
  const lookup = await resolve(caller);
64
- if (lookup.status !== "assigned")
68
+ if (lookup.status !== "assigned" && lookup.status !== "unassigned")
65
69
  throw new ForbiddenError(`Requires ${appId} permission ${permissions.join(" or ")}`);
70
+ if (permissionPolicy.allowOrgAdmins &&
71
+ (lookup.orgRole === "owner" || lookup.orgRole === "admin"))
72
+ return;
73
+ const roles = lookup.status === "assigned"
74
+ ? lookup.roles
75
+ : permissionPolicy.unassignedRole && !lookup.hasAssignments
76
+ ? [permissionPolicy.unassignedRole]
77
+ : [];
66
78
  const overrides = await getAppPermissionOverrides(appId, identity.orgId);
67
79
  const grants = permissions.flatMap((permission) => overrides[permission] ?? descriptor.permissions?.[permission] ?? []);
68
- if (!lookup.roles.some((role) => grants.includes(role))) {
80
+ if (!roles.some((role) => grants.includes(role))) {
69
81
  throw new ForbiddenError(`Requires ${appId} permission ${permissions.join(" or ")}`);
70
82
  }
71
83
  };
@@ -128,7 +140,7 @@ export async function resolveAppRole(descriptor, caller) {
128
140
  let rows;
129
141
  try {
130
142
  rows = (await getDbExec().execute({
131
- sql: `SELECT array_agg(r.role ORDER BY r.role) FILTER (WHERE r.role IS NOT NULL) AS roles,
143
+ sql: `SELECT m.role AS "orgRole", array_agg(r.role ORDER BY r.role) FILTER (WHERE r.role IS NOT NULL) AS roles,
132
144
  o.identity_authority AS "identityAuthority",
133
145
  o.identity_id AS "identityId"
134
146
  FROM org_members m
@@ -139,7 +151,7 @@ export async function resolveAppRole(descriptor, caller) {
139
151
  AND LOWER(r.email) = LOWER(m.email)
140
152
  WHERE m.org_id = ? AND LOWER(m.email) = ?
141
153
  AND m.federation_removal_pending_at IS NULL
142
- GROUP BY o.identity_authority, o.identity_id`,
154
+ GROUP BY m.role, o.identity_authority, o.identity_id`,
143
155
  args: [descriptor.appId, orgId, normalizeEmail(email)],
144
156
  })).rows;
145
157
  }
@@ -147,7 +159,7 @@ export async function resolveAppRole(descriptor, caller) {
147
159
  if (!isMissingOrganizationTableError(error))
148
160
  throw error;
149
161
  rows = (await getDbExec().execute({
150
- sql: `SELECT array_agg(r.role ORDER BY r.role) FILTER (WHERE r.role IS NOT NULL) AS roles
162
+ sql: `SELECT m.role AS "orgRole", array_agg(r.role ORDER BY r.role) FILTER (WHERE r.role IS NOT NULL) AS roles
151
163
  FROM org_members m
152
164
  LEFT JOIN app_member_roles r
153
165
  ON r.org_id = m.org_id
@@ -155,7 +167,7 @@ export async function resolveAppRole(descriptor, caller) {
155
167
  AND LOWER(r.email) = LOWER(m.email)
156
168
  WHERE m.org_id = ? AND LOWER(m.email) = ?
157
169
  AND m.federation_removal_pending_at IS NULL
158
- GROUP BY m.org_id, m.email`,
170
+ GROUP BY m.org_id, m.email, m.role`,
159
171
  args: [descriptor.appId, orgId, normalizeEmail(email)],
160
172
  })).rows;
161
173
  }
@@ -164,6 +176,7 @@ export async function resolveAppRole(descriptor, caller) {
164
176
  return { status: "not-a-member", orgId };
165
177
  const identityAuthority = String(row.identityAuthority ?? row.identity_authority ?? "").trim();
166
178
  const identityId = String(row.identityId ?? row.identity_id ?? "").trim();
179
+ let orgRole = typeof row.orgRole === "string" ? row.orgRole : undefined;
167
180
  if (identityAuthority || identityId) {
168
181
  const { validateFederatedOrganizationMembershipForCurrentRequest } = await import("./federation.js");
169
182
  const membership = await validateFederatedOrganizationMembershipForCurrentRequest({
@@ -172,15 +185,22 @@ export async function resolveAppRole(descriptor, caller) {
172
185
  });
173
186
  if (!membership.active)
174
187
  return { status: "not-a-member", orgId };
188
+ orgRole = membership.role;
175
189
  }
176
190
  const raw = row.roles ?? row.appRoles ?? row.approles;
177
191
  const roles = Array.isArray(raw) ? raw.map(String) : [];
178
192
  const validRoles = [
179
193
  ...new Set(roles.filter((role) => descriptor.roles.includes(role))),
180
194
  ];
195
+ const membership = orgRole ? { orgRole } : {};
181
196
  if (!validRoles.length)
182
- return { status: "unassigned", orgId };
183
- return { status: "assigned", roles: validRoles, orgId };
197
+ return {
198
+ status: "unassigned",
199
+ orgId,
200
+ ...membership,
201
+ ...(roles.length ? { hasAssignments: true } : {}),
202
+ };
203
+ return { status: "assigned", roles: validRoles, orgId, ...membership };
184
204
  }
185
205
  export async function listAppMemberRoles(appId, orgId) {
186
206
  const { rows } = await getDbExec().execute({
@@ -7,7 +7,7 @@ import { getDbExec, isTransientDatabaseError } from "../db/client.js";
7
7
  import { isAnonymousWaitlistSessionEmail } from "../server/anonymous-identity.js";
8
8
  import { crossSiteCookieAttrs, getSession } from "../server/auth.js";
9
9
  import { shouldWriteFirstRunOnboardingEligibility } from "../server/first-run-onboarding-build-mode.js";
10
- import { getRequestContext, hasExplicitPersonalOrgScope, } from "../server/request-context.js";
10
+ import { getVerifiedServiceIdentityFromEvent, getRequestContext, hasExplicitPersonalOrgScope, } from "../server/request-context.js";
11
11
  import { getSetting } from "../settings/store.js";
12
12
  import { getUserSetting } from "../settings/user-settings.js";
13
13
  import { FIRST_RUN_ONBOARDING_ELIGIBLE_KEY } from "../shared/first-run-onboarding.js";
@@ -170,6 +170,7 @@ async function resolveOrgContextUncached(event) {
170
170
  : null;
171
171
  const sessionOrgRole = normalizeOrgRole(session.orgRole);
172
172
  const requestContext = getRequestContext();
173
+ const verifiedServiceIdentity = getVerifiedServiceIdentityFromEvent(event);
173
174
  const serviceRole = implicitServiceOrgRole({
174
175
  email,
175
176
  orgId: sessionOrgId,
@@ -177,13 +178,28 @@ async function resolveOrgContextUncached(event) {
177
178
  ? null
178
179
  : (requestContext?.orgId ?? sessionOrgId),
179
180
  });
180
- if (serviceRole && sessionOrgId) {
181
- return {
182
- email,
183
- orgId: sessionOrgId,
184
- orgName: null,
185
- role: serviceRole,
186
- };
181
+ if (serviceRole &&
182
+ sessionOrgId &&
183
+ verifiedServiceIdentity &&
184
+ verifiedServiceIdentity.userEmail.trim().toLowerCase() ===
185
+ email.trim().toLowerCase() &&
186
+ verifiedServiceIdentity.orgId === sessionOrgId) {
187
+ const organization = await getDbExec().execute({
188
+ sql: `SELECT identity_authority, identity_id
189
+ FROM organizations WHERE id = ? LIMIT 1`,
190
+ args: [sessionOrgId],
191
+ });
192
+ const metadata = organization.rows[0];
193
+ if (metadata &&
194
+ !String(metadata.identity_authority ?? "").trim() &&
195
+ !String(metadata.identity_id ?? "").trim()) {
196
+ return {
197
+ email,
198
+ orgId: sessionOrgId,
199
+ orgName: null,
200
+ role: serviceRole,
201
+ };
202
+ }
187
203
  }
188
204
  const exec = getDbExec();
189
205
  const activeOrgSettingPromise = loadActiveOrgSettingForEvent(event, email);
@@ -23,7 +23,7 @@ export { registerIdentityColumns } from "../identity/rekey.js";
23
23
  export type { AppIdentityColumn, IdentityEmailChange, IdentityOffboard, IdentityOrgScope, } from "../identity/rekey.js";
24
24
  export { claimWorkspaceAppForOrganization, isStandaloneDispatchRuntime, isWorkspaceAppAccessAllowed, } from "./workspace-app-access.js";
25
25
  export { defineAppRoles, getRegisteredAppRoles, listRegisteredAppRoles, listAppMemberRoles, resolveAppRole, setAppMemberRole, setAppMemberRoles, applyInvitationAppRoles, getAppPermissionOverrides, setAppPermissionRoles, resolveAppAuthorizationContext, } from "./app-roles.js";
26
- export type { AppRoles, AppRolesDescriptor, AppRoleCaller, AppRoleLookup, AppMemberRoleRow, AppAuthorizationContext, } from "./app-roles.js";
26
+ export type { AppRoles, AppRolesDescriptor, AppRoleCaller, AppPermissionPolicy, AppRoleLookup, AppMemberRoleRow, AppAuthorizationContext, } from "./app-roles.js";
27
27
  export { ORG_MIGRATIONS } from "./migrations.js";
28
28
  export { CROSS_APP_ORG_FEDERATION_FLAG, CROSS_APP_ORG_FEDERATION_SCOPE, } from "./feature-flags.js";
29
29
  export declare const addFederatedOrganizationMember: typeof import("./federation.js").addFederatedOrganizationMember;
@@ -179,8 +179,8 @@ export declare const getMyOrgHandler: import("h3").EventHandlerWithFetch<import(
179
179
  export declare const createOrgHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
180
180
  name?: undefined;
181
181
  success: boolean;
182
- id?: undefined;
183
182
  role?: undefined;
183
+ id?: undefined;
184
184
  } | {
185
185
  success?: undefined;
186
186
  id: string;
@@ -0,0 +1 @@
1
+ export declare function resolveWorkspaceAccessAppId(targetAppId?: string): string;
@@ -0,0 +1,25 @@
1
+ import { getAppConfig } from "../app-config/index.js";
2
+ export function resolveWorkspaceAccessAppId(targetAppId) {
3
+ const app = getAppConfig().app;
4
+ const identities = [
5
+ app.id,
6
+ app.legacyId,
7
+ app.template,
8
+ app.slug,
9
+ app.packageName,
10
+ app.workspaceId,
11
+ ]
12
+ .map((value) => value?.trim())
13
+ .filter(Boolean);
14
+ // Other apps still require their own workspace grant; only this deployment's
15
+ // aliases resolve to the identity used by its HTTP access gate.
16
+ const target = targetAppId?.trim();
17
+ if (target && !identities.includes(target))
18
+ return target;
19
+ const workspaceId = app.workspaceId?.trim();
20
+ if (workspaceId)
21
+ return workspaceId;
22
+ return identities.some((value) => value?.toLowerCase() === "dispatch")
23
+ ? "dispatch"
24
+ : "";
25
+ }
@@ -2,6 +2,6 @@ export declare function createProgressHandler(): import("h3").EventHandlerWithFe
2
2
  error: string;
3
3
  ok?: undefined;
4
4
  } | {
5
- ok: boolean;
6
5
  error?: undefined;
6
+ ok: boolean;
7
7
  }>>;
@@ -75,9 +75,9 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
75
75
  user: "user";
76
76
  }>>;
77
77
  }, z.core.$strip>>, {
78
+ deleted?: undefined;
78
79
  message?: undefined;
79
80
  id?: undefined;
80
- found?: undefined;
81
81
  providers: {
82
82
  id: string;
83
83
  label: string;
@@ -89,45 +89,45 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
89
89
  }[];
90
90
  count: number;
91
91
  provider?: undefined;
92
- deleted?: undefined;
92
+ found?: undefined;
93
93
  registered?: undefined;
94
94
  label?: undefined;
95
95
  } | {
96
+ deleted?: undefined;
96
97
  message?: undefined;
97
98
  id?: undefined;
98
- count?: undefined;
99
99
  providers?: undefined;
100
+ count?: undefined;
100
101
  found: boolean;
101
102
  provider: import("../custom-registry.js").CustomProviderConfig;
102
- deleted?: undefined;
103
103
  registered?: undefined;
104
104
  label?: undefined;
105
105
  } | {
106
+ deleted?: undefined;
106
107
  message?: undefined;
107
- count?: undefined;
108
108
  providers?: undefined;
109
+ count?: undefined;
109
110
  provider?: undefined;
110
111
  found: boolean;
111
112
  id: string;
112
- deleted?: undefined;
113
113
  registered?: undefined;
114
114
  label?: undefined;
115
115
  } | {
116
116
  message?: undefined;
117
- count?: undefined;
118
- found?: undefined;
119
117
  providers?: undefined;
118
+ count?: undefined;
120
119
  provider?: undefined;
120
+ found?: undefined;
121
121
  deleted: boolean;
122
122
  id: string;
123
123
  registered?: undefined;
124
124
  label?: undefined;
125
125
  } | {
126
- count?: undefined;
127
- found?: undefined;
126
+ deleted?: undefined;
128
127
  providers?: undefined;
128
+ count?: undefined;
129
129
  provider?: undefined;
130
- deleted?: undefined;
130
+ found?: undefined;
131
131
  registered: boolean;
132
132
  id: string;
133
133
  label: string;
@@ -315,7 +315,6 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
315
315
  provider?: undefined;
316
316
  message?: undefined;
317
317
  deleted?: undefined;
318
- found?: undefined;
319
318
  providers: {
320
319
  id: string;
321
320
  label: string;
@@ -326,6 +325,7 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
326
325
  updatedAt: number;
327
326
  }[];
328
327
  count: number;
328
+ found?: undefined;
329
329
  registered?: undefined;
330
330
  label?: undefined;
331
331
  } | {
@@ -352,8 +352,8 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
352
352
  provider?: undefined;
353
353
  count?: undefined;
354
354
  message?: undefined;
355
- found?: undefined;
356
355
  providers?: undefined;
356
+ found?: undefined;
357
357
  deleted: boolean;
358
358
  id: string;
359
359
  registered?: undefined;
@@ -362,8 +362,8 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
362
362
  provider?: undefined;
363
363
  count?: undefined;
364
364
  deleted?: undefined;
365
- found?: undefined;
366
365
  providers?: undefined;
366
+ found?: undefined;
367
367
  registered: boolean;
368
368
  id: string;
369
369
  label: string;