@hexclave/js 1.0.70 → 1.0.72

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 (91) hide show
  1. package/dist/{admin-app-impl-DxZSp6ov.d.ts → admin-app-impl-BBSNNr6S.d.ts} +41 -2
  2. package/dist/admin-app-impl-BBSNNr6S.d.ts.map +1 -0
  3. package/dist/clickmap/clickmap-core.d.ts +1 -1
  4. package/dist/clickmap/index.d.ts +1 -1
  5. package/dist/{client-app-DBcv6rRn.d.ts → client-app-C42iaDoZ.d.ts} +41 -2
  6. package/dist/client-app-C42iaDoZ.d.ts.map +1 -0
  7. package/dist/dev-tool/dev-tool-core.d.ts +1 -1
  8. package/dist/dev-tool/index.d.ts +1 -1
  9. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +39 -0
  10. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
  11. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js +98 -0
  12. package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  13. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +3 -3
  14. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  15. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
  16. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +3 -4
  17. package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  18. package/dist/esm/lib/hexclave-app/apps/implementations/common.js +2 -2
  19. package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.d.ts.map +1 -1
  20. package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.js +2 -2
  21. package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.js.map +1 -1
  22. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts +41 -2
  23. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
  24. package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
  25. package/dist/esm/lib/hexclave-app/index.d.ts +2 -1
  26. package/dist/esm/lib/hexclave-app/passkey-rp-id.d.ts +12 -0
  27. package/dist/esm/lib/hexclave-app/passkey-rp-id.d.ts.map +1 -0
  28. package/dist/esm/lib/hexclave-app/passkey-rp-id.js +15 -0
  29. package/dist/esm/lib/hexclave-app/passkey-rp-id.js.map +1 -0
  30. package/dist/esm/lib/hexclave-app/passkey-rp-id.test.d.ts +1 -0
  31. package/dist/esm/lib/hexclave-app/passkey-rp-id.test.js +26 -0
  32. package/dist/esm/lib/hexclave-app/passkey-rp-id.test.js.map +1 -0
  33. package/dist/esm/lib/hexclave-app/workflows.d.ts +125 -0
  34. package/dist/esm/lib/hexclave-app/workflows.d.ts.map +1 -0
  35. package/dist/esm/lib/hexclave-app/workflows.js +110 -0
  36. package/dist/esm/lib/hexclave-app/workflows.js.map +1 -0
  37. package/dist/esm/lib/hexclave-app/workflows.test.d.ts +1 -0
  38. package/dist/esm/lib/hexclave-app/workflows.test.js +25 -0
  39. package/dist/esm/lib/hexclave-app/workflows.test.js.map +1 -0
  40. package/dist/index.d.ts +3 -2
  41. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +1 -1
  42. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js +98 -0
  43. package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
  44. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js +3 -3
  45. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.js.map +1 -1
  46. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -1
  47. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +3 -4
  48. package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
  49. package/dist/lib/hexclave-app/apps/implementations/common.js +2 -2
  50. package/dist/lib/hexclave-app/apps/implementations/index.d.ts +1 -1
  51. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
  52. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.js +2 -2
  53. package/dist/lib/hexclave-app/apps/implementations/server-app-impl.js.map +1 -1
  54. package/dist/lib/hexclave-app/apps/index.d.ts +1 -1
  55. package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts +3 -2
  56. package/dist/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
  57. package/dist/lib/hexclave-app/apps/interfaces/client-app.d.ts +1 -1
  58. package/dist/lib/hexclave-app/apps/interfaces/server-app.d.ts +1 -1
  59. package/dist/lib/hexclave-app/index.d.ts +3 -2
  60. package/dist/lib/hexclave-app/passkey-rp-id.d.ts +12 -0
  61. package/dist/lib/hexclave-app/passkey-rp-id.d.ts.map +1 -0
  62. package/dist/lib/hexclave-app/passkey-rp-id.js +17 -0
  63. package/dist/lib/hexclave-app/passkey-rp-id.js.map +1 -0
  64. package/dist/lib/hexclave-app/passkey-rp-id.test.d.ts +1 -0
  65. package/dist/lib/hexclave-app/passkey-rp-id.test.js +25 -0
  66. package/dist/lib/hexclave-app/passkey-rp-id.test.js.map +1 -0
  67. package/dist/lib/hexclave-app/projects/index.d.ts +1 -1
  68. package/dist/lib/hexclave-app/teams/index.d.ts +1 -1
  69. package/dist/lib/hexclave-app/users/index.d.ts +1 -1
  70. package/dist/lib/hexclave-app/workflows.d.ts +2 -0
  71. package/dist/lib/hexclave-app/workflows.js +116 -0
  72. package/dist/lib/hexclave-app/workflows.js.map +1 -0
  73. package/dist/lib/hexclave-app/workflows.test.d.ts +1 -0
  74. package/dist/lib/hexclave-app/workflows.test.js +24 -0
  75. package/dist/lib/hexclave-app/workflows.test.js.map +1 -0
  76. package/dist/pushed-config-error-overlay/index.d.ts +1 -1
  77. package/dist/workflows-DEb7pIuQ.d.ts +125 -0
  78. package/dist/workflows-DEb7pIuQ.d.ts.map +1 -0
  79. package/package.json +2 -2
  80. package/src/lib/hexclave-app/apps/implementations/admin-app-impl.ts +119 -0
  81. package/src/lib/hexclave-app/apps/implementations/client-app-impl.cross-domain.test.ts +3 -3
  82. package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +10 -11
  83. package/src/lib/hexclave-app/apps/implementations/server-app-impl.ts +2 -7
  84. package/src/lib/hexclave-app/apps/interfaces/admin-app.ts +19 -0
  85. package/src/lib/hexclave-app/index.ts +15 -0
  86. package/src/lib/hexclave-app/passkey-rp-id.test.ts +43 -0
  87. package/src/lib/hexclave-app/passkey-rp-id.ts +27 -0
  88. package/src/lib/hexclave-app/workflows.test.ts +28 -0
  89. package/src/lib/hexclave-app/workflows.ts +245 -0
  90. package/dist/admin-app-impl-DxZSp6ov.d.ts.map +0 -1
  91. package/dist/client-app-DBcv6rRn.d.ts.map +0 -1
@@ -25,6 +25,7 @@ import { AdminProjectPermission, AdminProjectPermissionDefinition, AdminProjectP
25
25
  import type { PlanUsage } from "../../plan-usage";
26
26
  import { AdminOwnedProject, AdminProject, AdminProjectUpdateOptions, PushConfigOptions, adminProjectUpdateOptionsToCrud } from "../../projects";
27
27
  import type { AdminSessionReplay, AdminSessionReplayChunk, ListSessionReplayChunksOptions, ListSessionReplayChunksResult, ListSessionReplaysOptions, ListSessionReplaysResult, SessionReplayAllEventsResult } from "../../session-replays";
28
+ import { AdminWorkflow, AdminWorkflowRun, AdminWorkflowRunDetails, AdminWorkflowRunsFilter, AdminWorkflowSyncResult, AdminWorkflowUpgradeResult, AdminWorkflowVersion, adminWorkflowFromCrud, adminWorkflowRunDetailsFromCrud, adminWorkflowRunFromCrud, adminWorkflowSyncResultFromCrud, adminWorkflowVersionFromCrud, isWorkflowRunDetailsJson } from "../../workflows";
28
29
  import { ManagedEmailProviderListItem, ManagedEmailProviderSetupResult, ManagedEmailProviderStatus, EmailOutboxUpdateOptions, StackAdminApp, StackAdminAppConstructorOptions } from "../interfaces/admin-app";
29
30
  import { clientVersion, createCache, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getDefaultSecretServerKey, getDefaultSuperSecretAdminKey, resolveApiUrls, resolveConstructorOptions } from "./common";
30
31
  import { _HexclaveServerAppImplIncomplete } from "./server-app-impl";
@@ -97,6 +98,9 @@ export class _HexclaveAdminAppImplIncomplete<HasTokenStore extends boolean, Proj
97
98
  private readonly _adminEmailDraftsCache = createCache(async () => {
98
99
  return await this._interface.listInternalEmailDrafts();
99
100
  });
101
+ private readonly _adminWorkflowsCache = createCache(async () => {
102
+ return await this._interface.listWorkflows();
103
+ });
100
104
  private readonly _adminTeamPermissionDefinitionsCache = createCache(async () => {
101
105
  return await this._interface.listTeamPermissionDefinitions();
102
106
  });
@@ -477,6 +481,121 @@ export class _HexclaveAdminAppImplIncomplete<HasTokenStore extends boolean, Proj
477
481
  }));
478
482
  }
479
483
 
484
+ // ─── Workflows (internal-project gated; see the Workflows v1 spec) ───────
485
+
486
+ async listWorkflows(): Promise<AdminWorkflow[]> {
487
+ const crud = Result.orThrow(await this._adminWorkflowsCache.getOrWait([], "write-only"));
488
+ return crud.map(adminWorkflowFromCrud);
489
+ }
490
+
491
+ async createWorkflow(options: { id: string, displayName?: string, source: string }): Promise<AdminWorkflowSyncResult> {
492
+ const result = await this._interface.createWorkflow({
493
+ id: options.id,
494
+ display_name: options.displayName,
495
+ source: options.source,
496
+ });
497
+ await this._adminWorkflowsCache.refresh([]);
498
+ return adminWorkflowSyncResultFromCrud(result);
499
+ }
500
+
501
+ async updateWorkflowSource(workflowId: string, source: string): Promise<AdminWorkflowSyncResult> {
502
+ const result = await this._interface.updateWorkflowSource(workflowId, source);
503
+ await this._adminWorkflowsCache.refresh([]);
504
+ return adminWorkflowSyncResultFromCrud(result);
505
+ }
506
+
507
+ async deleteWorkflow(workflowId: string): Promise<void> {
508
+ await this._interface.deleteWorkflow(workflowId);
509
+ await this._adminWorkflowsCache.refresh([]);
510
+ }
511
+
512
+ async listWorkflowVersions(workflowId: string): Promise<AdminWorkflowVersion[]> {
513
+ return (await this._interface.listWorkflowVersions(workflowId)).map(adminWorkflowVersionFromCrud);
514
+ }
515
+
516
+ async listWorkflowRuns(workflowId: string, filter: AdminWorkflowRunsFilter & { includeState: true }): Promise<{ runs: AdminWorkflowRunDetails[], nextCursor: string | null }>;
517
+ async listWorkflowRuns(workflowId: string, filter?: AdminWorkflowRunsFilter): Promise<{ runs: AdminWorkflowRun[], nextCursor: string | null }>;
518
+ async listWorkflowRuns(workflowId: string, filter: AdminWorkflowRunsFilter = {}): Promise<{ runs: AdminWorkflowRun[], nextCursor: string | null }> {
519
+ if (filter.includeState === true) {
520
+ const result = await this._interface.listWorkflowRuns(workflowId, {
521
+ state: filter.state,
522
+ version: filter.version,
523
+ run_key: filter.runKey,
524
+ cursor: filter.cursor,
525
+ limit: filter.limit,
526
+ include_state: true,
527
+ });
528
+ return {
529
+ runs: result.runs.map((run) => adminWorkflowRunDetailsFromCrud(
530
+ isWorkflowRunDetailsJson(run)
531
+ ? run
532
+ : throwErr("Workflow runs response omitted state after include_state=true"),
533
+ )),
534
+ nextCursor: result.next_cursor,
535
+ };
536
+ }
537
+ const result = await this._interface.listWorkflowRuns(workflowId, {
538
+ state: filter.state,
539
+ version: filter.version,
540
+ run_key: filter.runKey,
541
+ cursor: filter.cursor,
542
+ limit: filter.limit,
543
+ });
544
+ return {
545
+ runs: result.runs.map(adminWorkflowRunFromCrud),
546
+ nextCursor: result.next_cursor,
547
+ };
548
+ }
549
+
550
+ async getWorkflowRun(runId: string): Promise<AdminWorkflowRunDetails> {
551
+ return adminWorkflowRunDetailsFromCrud(await this._interface.getWorkflowRun(runId));
552
+ }
553
+
554
+ async cancelWorkflowRuns(workflowId: string, filter: { runKey?: string, runId?: string, state?: "queued" | "running" | "sleeping", version?: number } = {}): Promise<{ canceledCount: number }> {
555
+ const result = await this._interface.cancelWorkflowRuns(workflowId, {
556
+ run_key: filter.runKey,
557
+ run_id: filter.runId,
558
+ state: filter.state,
559
+ version: filter.version,
560
+ });
561
+ await this._adminWorkflowsCache.refresh([]);
562
+ return { canceledCount: result.canceled_count };
563
+ }
564
+
565
+ async upgradeWorkflowRuns(workflowId: string, options: { toVersion: number, runKey?: string, fromVersion?: number }): Promise<AdminWorkflowUpgradeResult> {
566
+ const result = await this._interface.upgradeWorkflowRuns(workflowId, {
567
+ to_version: options.toVersion,
568
+ run_key: options.runKey,
569
+ from_version: options.fromVersion,
570
+ });
571
+ return {
572
+ upgradedCount: result.upgraded_count,
573
+ skipped: result.skipped.map((skip) => ({
574
+ runId: skip.run_id,
575
+ runKey: skip.run_key,
576
+ fromVersion: skip.from_version,
577
+ diagnostic: {
578
+ reason: skip.diagnostic.reason,
579
+ suspendedStepKey: skip.diagnostic.suspended_step_key,
580
+ foundStepKey: skip.diagnostic.found_step_key,
581
+ consumedStepKeys: skip.diagnostic.consumed_step_keys,
582
+ unconsumedStepKeys: skip.diagnostic.unconsumed_step_keys,
583
+ details: skip.diagnostic.details,
584
+ },
585
+ })),
586
+ };
587
+ }
588
+
589
+ async retryWorkflowRun(runId: string): Promise<void> {
590
+ await this._interface.retryWorkflowRun(runId);
591
+ await this._adminWorkflowsCache.refresh([]);
592
+ }
593
+
594
+ async sendWorkflowEvent(name: string, data?: unknown): Promise<{ eventId: string }> {
595
+ const result = await this._interface.sendWorkflowEvent(name, data ?? null);
596
+ return { eventId: result.event_id };
597
+ }
598
+
480
599
  async listEmailDrafts(): Promise<{ id: string, displayName: string, themeId: string | undefined | false, tsxSource: string, sentAt: Date | null }[]> {
481
600
  const crud = Result.orThrow(await this._adminEmailDraftsCache.getOrWait([], "write-only"));
482
601
  return crud.map((draft) => ({
@@ -535,7 +535,7 @@ describe("StackClientApp cross-domain auth", () => {
535
535
  }
536
536
 
537
537
  const errorUrl = new URL(redirectedUrl);
538
- expect(errorUrl.origin).toBe(`https://${projectId}.built-with-stack-auth.com`);
538
+ expect(errorUrl.origin).toBe(`https://${projectId}.built-with-hexclave.com`);
539
539
  expect(errorUrl.pathname).toBe("/handler/error");
540
540
  expect(errorUrl.searchParams.get("errorCode")).toBe("SIGN_UP_REJECTED");
541
541
  expect(errorUrl.searchParams.get("message")).toBe("Your sign up was rejected by an administrator's sign-up rule.");
@@ -715,7 +715,7 @@ describe("StackClientApp cross-domain auth", () => {
715
715
  redirectBackUrl.searchParams.set("hexclave_cross_domain_code_challenge", handoffCodeChallenge);
716
716
  redirectBackUrl.searchParams.set("hexclave_cross_domain_after_callback_redirect_url", handoffAfterCallbackRedirect);
717
717
 
718
- const arrivalUrl = new URL(`https://${projectId}.built-with-stack-auth.com/handler/sign-in`);
718
+ const arrivalUrl = new URL(`https://${projectId}.built-with-hexclave.com/handler/sign-in`);
719
719
  arrivalUrl.searchParams.set("after_auth_return_to", redirectBackUrl.toString());
720
720
  arrivalUrl.searchParams.set("hexclave_cross_domain_state", handoffState);
721
721
  arrivalUrl.searchParams.set("hexclave_cross_domain_code_challenge", handoffCodeChallenge);
@@ -754,7 +754,7 @@ describe("StackClientApp cross-domain auth", () => {
754
754
  .mockResolvedValue(crossDomainAuthorizeRedirect);
755
755
 
756
756
  // All redirect-back query params were dropped before the after-sign-in redirect.
757
- windowMock.location.href = `https://${projectId}.built-with-stack-auth.com/handler/sign-in`;
757
+ windowMock.location.href = `https://${projectId}.built-with-hexclave.com/handler/sign-in`;
758
758
 
759
759
  const redirectUrl = await clientApp[hexclaveAppInternalsSymbol].getRedirectToHandlerUrl("afterSignIn");
760
760
 
@@ -59,6 +59,7 @@ import { EditableTeamMemberProfile, ReceivedTeamInvitation, SentTeamInvitation,
59
59
  import { buildCliAuthConfirmUrl, getHostedHandlerUrl, isHostedHandlerUrlForProject, resolveHandlerUrls } from "../../url-targets";
60
60
  import { augmentUrlWithPersistedRedirectBackState, saveRedirectBackStateFromUrl } from "./redirect-back-state";
61
61
  import { recordRedirectAndThrowIfLoopDetected } from "./redirect-loop-breaker";
62
+ import { scopePasskeyAuthenticationToHostname, scopePasskeyRegistrationToHostname } from "../../passkey-rp-id";
62
63
  import { ActiveSession, Auth, BaseUser, CurrentUser, InternalUserExtra, OAuthProvider, ProjectCurrentUser, SyncedPartialUser, TokenPartialUser, UserExtra, UserUpdateOptions, userUpdateOptionsToCrud, withUserDestructureGuard } from "../../users";
63
64
  import { StackClientApp, StackClientAppConstructorOptions, StackClientAppJson } from "../interfaces/client-app";
64
65
  import { _HexclaveAdminAppImplIncomplete } from "./admin-app-impl";
@@ -2233,12 +2234,11 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
2233
2234
 
2234
2235
  const { options_json, code } = initiationResult.data;
2235
2236
 
2236
- // HACK: Override the rpID to be the actual domain
2237
- if (options_json.rp.id !== "THIS_VALUE_WILL_BE_REPLACED.example.com") {
2238
- throw new HexclaveAssertionError(`Expected returned RP ID from server to equal sentinel, but found ${options_json.rp.id}`);
2239
- }
2240
-
2241
- options_json.rp.id = hostname;
2237
+ // Passkeys intentionally stay scoped to the hostname where they were
2238
+ // registered. In particular, passkeys created on the legacy hosted
2239
+ // components domain continue to work there but are not carried over to
2240
+ // a replacement hosted domain.
2241
+ scopePasskeyRegistrationToHostname(options_json, hostname);
2242
2242
 
2243
2243
  let attResp;
2244
2244
  try {
@@ -3425,11 +3425,10 @@ export class _HexclaveClientAppImplIncomplete<HasTokenStore extends boolean, Pro
3425
3425
 
3426
3426
  const { options_json, code } = initiationResult.data;
3427
3427
 
3428
- // HACK: Override the rpID to be the actual domain
3429
- if (options_json.rpId !== "THIS_VALUE_WILL_BE_REPLACED.example.com") {
3430
- throw new HexclaveAssertionError(`Expected returned RP ID from server to equal sentinel, but found ${options_json.rpId}`);
3431
- }
3432
- options_json.rpId = window.location.hostname;
3428
+ // Keep authentication scoped to the current hostname. This deliberately
3429
+ // does not add cross-domain compatibility for passkeys created on a
3430
+ // previous hosted components domain.
3431
+ scopePasskeyAuthenticationToHostname(options_json, window.location.hostname);
3433
3432
 
3434
3433
  const authentication_response = await startAuthentication({ optionsJSON: options_json });
3435
3434
  return await this._interface.signInWithPasskey({ authentication_response, code }, session);
@@ -35,6 +35,7 @@ import { Customer, CustomerProductsList, CustomerProductsRequestOptions, InlineP
35
35
  import { DataVaultStore } from "../../data-vault";
36
36
  import { EmailDeliveryInfo, SendEmailOptions } from "../../email";
37
37
  import { NotificationCategory } from "../../notification-categories";
38
+ import { scopePasskeyRegistrationToHostname } from "../../passkey-rp-id";
38
39
  import { AdminProjectPermissionDefinition, AdminTeamPermission, AdminTeamPermissionDefinition } from "../../permissions";
39
40
  import { EditableTeamMemberProfile, ReceivedTeamInvitation, SentTeamInvitation, ServerListTeamsOptions, ServerListUsersOptions, ServerTeam, ServerTeamCreateOptions, ServerTeamUpdateOptions, ServerTeamUser, Team, serverTeamCreateOptionsToCrud, serverTeamUpdateOptionsToCrud } from "../../teams";
40
41
  import { ProjectCurrentServerUser, ServerOAuthProvider, ServerUser, ServerUserCreateOptions, ServerUserUpdateOptions, serverUserCreateOptionsToCrud, serverUserUpdateOptionsToCrud, withUserDestructureGuard } from "../../users";
@@ -780,7 +781,6 @@ export class _HexclaveServerAppImplIncomplete<HasTokenStore extends boolean, Pro
780
781
  return providers.find((p) => p.id === id) ?? null;
781
782
  },
782
783
  async registerPasskey(options?: { hostname?: string }): Promise<Result<undefined, KnownErrors["PasskeyRegistrationFailed"] | KnownErrors["PasskeyWebAuthnError"]>> {
783
- // TODO remove duplicated code between this and the function in client-app-impl.ts
784
784
  const hostname = options?.hostname || (await app._getCurrentUrl())?.hostname;
785
785
  if (!hostname) {
786
786
  throw new HexclaveAssertionError("hostname must be provided if the Stack App does not have a redirect method");
@@ -795,12 +795,7 @@ export class _HexclaveServerAppImplIncomplete<HasTokenStore extends boolean, Pro
795
795
 
796
796
  const { options_json, code } = initiationResult.data;
797
797
 
798
- // HACK: Override the rpID to be the actual domain
799
- if (options_json.rp.id !== "THIS_VALUE_WILL_BE_REPLACED.example.com") {
800
- throw new HexclaveAssertionError(`Expected returned RP ID from server to equal sentinel, but found ${options_json.rp.id}`);
801
- }
802
-
803
- options_json.rp.id = hostname;
798
+ scopePasskeyRegistrationToHostname(options_json, hostname);
804
799
 
805
800
  let attResp;
806
801
  try {
@@ -14,6 +14,7 @@ import { InternalApiKey, InternalApiKeyCreateOptions, InternalApiKeyFirstView }
14
14
  import { AdminProjectPermission, AdminProjectPermissionDefinition, AdminProjectPermissionDefinitionCreateOptions, AdminProjectPermissionDefinitionUpdateOptions, AdminTeamPermission, AdminTeamPermissionDefinition, AdminTeamPermissionDefinitionCreateOptions, AdminTeamPermissionDefinitionUpdateOptions } from "../../permissions";
15
15
  import type { PlanUsage } from "../../plan-usage";
16
16
  import { AdminProject } from "../../projects";
17
+ import type { AdminWorkflow, AdminWorkflowRun, AdminWorkflowRunDetails, AdminWorkflowRunsFilter, AdminWorkflowSyncResult, AdminWorkflowUpgradeResult, AdminWorkflowVersion } from "../../workflows";
17
18
  import { _HexclaveAdminAppImpl } from "../implementations";
18
19
  import { StackServerApp, StackServerAppConstructorOptions } from "./server-app";
19
20
 
@@ -60,6 +61,7 @@ export type ManagedEmailProviderListItem = {
60
61
 
61
62
  import type { AdminSessionReplay, ListSessionReplayChunksOptions, ListSessionReplayChunksResult, ListSessionReplaysOptions, ListSessionReplaysResult, SessionReplayAllEventsResult } from "../../session-replays";
62
63
  export type { AdminSessionReplay, AdminSessionReplayChunk, ListSessionReplaysOptions, ListSessionReplaysResult, ListSessionReplayChunksOptions, ListSessionReplayChunksResult, SessionReplayAllEventsResult } from "../../session-replays";
64
+ export type { AdminWorkflow, AdminWorkflowDivergenceDiagnostic, AdminWorkflowRun, AdminWorkflowRunDetails, AdminWorkflowRunsFilter, AdminWorkflowRunState, AdminWorkflowStep, AdminWorkflowStepAttempt, AdminWorkflowSyncResult, AdminWorkflowTrigger, AdminWorkflowUpgradeResult, AdminWorkflowVersion } from "../../workflows";
63
65
 
64
66
 
65
67
  /** @deprecated Use `HexclaveAdminAppConstructorOptions` from the `@hexclave/*` package instead — same symbol, new brand name. See https://docs.hexclave.com/migration. */
@@ -83,6 +85,7 @@ export type StackAdminApp<HasTokenStore extends boolean = boolean, ProjectId ext
83
85
  & AsyncStoreProperty<"emailPreview", [{ themeId?: string | null | false, themeTsxSource?: string, templateId?: string, templateTsxSource?: string }], string, false>
84
86
  & AsyncStoreProperty<"emailTemplates", [], { id: string, displayName: string, themeId?: string, tsxSource: string }[], true>
85
87
  & AsyncStoreProperty<"emailDrafts", [], { id: string, displayName: string, themeId: string | undefined | false, tsxSource: string, sentAt: Date | null }[], true>
88
+ & AsyncStoreProperty<"workflows", [], AdminWorkflow[], true>
86
89
  & AsyncStoreProperty<"stripeAccountInfo", [], { account_id: string, charges_enabled: boolean, details_submitted: boolean, payouts_enabled: boolean } | null, false>
87
90
  & AsyncStoreProperty<
88
91
  "transactions",
@@ -138,6 +141,22 @@ export type StackAdminApp<HasTokenStore extends boolean = boolean, ProjectId ext
138
141
  createEmailTemplate(displayName: string): Promise<{ id: string }>,
139
142
  deleteEmailTemplate(id: string): Promise<void>,
140
143
 
144
+ // Workflows (internal-project gated; see the Workflows v1 spec). The
145
+ // management verbs live on the admin app only in v1.
146
+ createWorkflow(options: { id: string, displayName?: string, source: string }): Promise<AdminWorkflowSyncResult>,
147
+ updateWorkflowSource(workflowId: string, source: string): Promise<AdminWorkflowSyncResult>,
148
+ deleteWorkflow(workflowId: string): Promise<void>,
149
+ listWorkflowVersions(workflowId: string): Promise<AdminWorkflowVersion[]>,
150
+ listWorkflowRuns: {
151
+ (workflowId: string, filter: AdminWorkflowRunsFilter & { includeState: true }): Promise<{ runs: AdminWorkflowRunDetails[], nextCursor: string | null }>,
152
+ (workflowId: string, filter?: AdminWorkflowRunsFilter): Promise<{ runs: AdminWorkflowRun[], nextCursor: string | null }>,
153
+ },
154
+ getWorkflowRun(runId: string): Promise<AdminWorkflowRunDetails>,
155
+ cancelWorkflowRuns(workflowId: string, filter?: { runKey?: string, runId?: string, state?: "queued" | "running" | "sleeping", version?: number }): Promise<{ canceledCount: number }>,
156
+ upgradeWorkflowRuns(workflowId: string, options: { toVersion: number, runKey?: string, fromVersion?: number }): Promise<AdminWorkflowUpgradeResult>,
157
+ retryWorkflowRun(runId: string): Promise<void>,
158
+ sendWorkflowEvent(name: string, data?: unknown): Promise<{ eventId: string }>,
159
+
141
160
  setupPayments(): Promise<{ url: string }>,
142
161
  createStripeWidgetAccountSession(): Promise<{ client_secret: string }>,
143
162
  getPaymentMethodConfigs(): Promise<{ configId: string, methods: Array<{ id: string, name: string, enabled: boolean, available: boolean, overridable: boolean }> } | null>,
@@ -45,6 +45,21 @@ export type {
45
45
  EmailOutboxUpdateOptions
46
46
  } from "./apps/interfaces/admin-app";
47
47
 
48
+ export type {
49
+ AdminWorkflow,
50
+ AdminWorkflowDivergenceDiagnostic,
51
+ AdminWorkflowRun,
52
+ AdminWorkflowRunDetails,
53
+ AdminWorkflowRunsFilter,
54
+ AdminWorkflowRunState,
55
+ AdminWorkflowStep,
56
+ AdminWorkflowStepAttempt,
57
+ AdminWorkflowSyncResult,
58
+ AdminWorkflowTrigger,
59
+ AdminWorkflowUpgradeResult,
60
+ AdminWorkflowVersion
61
+ } from "./workflows";
62
+
48
63
  export type {
49
64
  ProjectConfig
50
65
  } from "./project-configs";
@@ -0,0 +1,43 @@
1
+
2
+ //===========================================
3
+ // THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
4
+ //===========================================
5
+ import { describe, expect, it } from "vitest";
6
+ import { scopePasskeyAuthenticationToHostname, scopePasskeyRegistrationToHostname } from "./passkey-rp-id";
7
+
8
+ const passkeyRpIdSentinel = "THIS_VALUE_WILL_BE_REPLACED.example.com";
9
+
10
+ describe.each([
11
+ "project-id.built-with-hexclave.com",
12
+ "project-id.built-with-stack-auth.com",
13
+ ])("passkeys on %s", (hostname) => {
14
+ it("scopes registration to the current hostname", () => {
15
+ const options = { rp: { id: passkeyRpIdSentinel } };
16
+
17
+ scopePasskeyRegistrationToHostname(options, hostname);
18
+
19
+ expect(options.rp.id).toBe(hostname);
20
+ });
21
+
22
+ it("scopes authentication to the current hostname", () => {
23
+ const options = { rpId: passkeyRpIdSentinel };
24
+
25
+ scopePasskeyAuthenticationToHostname(options, hostname);
26
+
27
+ expect(options.rpId).toBe(hostname);
28
+ });
29
+ });
30
+
31
+ it("rejects registration options without the server sentinel", () => {
32
+ expect(() => scopePasskeyRegistrationToHostname(
33
+ { rp: { id: "unexpected.example.com" } },
34
+ "project-id.built-with-hexclave.com",
35
+ )).toThrow("Expected returned RP ID from server to equal sentinel");
36
+ });
37
+
38
+ it("rejects authentication options without the server sentinel", () => {
39
+ expect(() => scopePasskeyAuthenticationToHostname(
40
+ { rpId: "unexpected.example.com" },
41
+ "project-id.built-with-hexclave.com",
42
+ )).toThrow("Expected returned RP ID from server to equal sentinel");
43
+ });
@@ -0,0 +1,27 @@
1
+
2
+ //===========================================
3
+ // THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
4
+ //===========================================
5
+ import { HexclaveAssertionError } from "@hexclave/shared/dist/utils/errors";
6
+
7
+ const passkeyRpIdSentinel = "THIS_VALUE_WILL_BE_REPLACED.example.com";
8
+
9
+ export function scopePasskeyRegistrationToHostname(
10
+ options: { rp: { id?: string } },
11
+ hostname: string,
12
+ ): void {
13
+ if (options.rp.id !== passkeyRpIdSentinel) {
14
+ throw new HexclaveAssertionError(`Expected returned RP ID from server to equal sentinel, but found ${options.rp.id}`);
15
+ }
16
+ options.rp.id = hostname;
17
+ }
18
+
19
+ export function scopePasskeyAuthenticationToHostname(
20
+ options: { rpId?: string },
21
+ hostname: string,
22
+ ): void {
23
+ if (options.rpId !== passkeyRpIdSentinel) {
24
+ throw new HexclaveAssertionError(`Expected returned RP ID from server to equal sentinel, but found ${options.rpId}`);
25
+ }
26
+ options.rpId = hostname;
27
+ }
@@ -0,0 +1,28 @@
1
+
2
+ //===========================================
3
+ // THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
4
+ //===========================================
5
+ import type { WorkflowVersionJson } from "@hexclave/shared/dist/interface/workflows";
6
+ import { describe, expect, it } from "vitest";
7
+ import { adminWorkflowVersionFromCrud } from "./workflows";
8
+
9
+ describe("adminWorkflowVersionFromCrud", () => {
10
+ it("preserves the deployment source hash", () => {
11
+ const version: WorkflowVersionJson = {
12
+ workflow_id: "daily-report",
13
+ version: 3,
14
+ source: "export default 1",
15
+ source_hash: "0123456789abcdef",
16
+ runtime_env_version: "v1",
17
+ is_latest: true,
18
+ in_flight_runs: 2,
19
+ created_at_millis: 1234,
20
+ };
21
+
22
+ expect(adminWorkflowVersionFromCrud(version)).toMatchObject({
23
+ workflowId: "daily-report",
24
+ version: 3,
25
+ sourceHash: "0123456789abcdef",
26
+ });
27
+ });
28
+ });
@@ -0,0 +1,245 @@
1
+
2
+ //===========================================
3
+ // THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template
4
+ //===========================================
5
+ import type {
6
+ WorkflowDivergenceDiagnosticJson,
7
+ WorkflowRunDetailsJson,
8
+ WorkflowRunJson,
9
+ WorkflowSummaryJson,
10
+ WorkflowSyncResultJson,
11
+ WorkflowVersionJson,
12
+ } from "@hexclave/shared/dist/interface/workflows";
13
+
14
+ // SDK-side (camelCase) types + converters for the workflows admin surface.
15
+ // The wire types live in @hexclave/shared/dist/interface/workflows; these
16
+ // mirror them 1:1 so the dashboard never touches snake_case.
17
+
18
+ export type AdminWorkflowTrigger =
19
+ | { type: "event", eventType: string }
20
+ | { type: "schedule", cron: string, timezone: string };
21
+
22
+ export type AdminWorkflow = {
23
+ id: string,
24
+ displayName: string,
25
+ latestVersion: number,
26
+ triggers: AdminWorkflowTrigger[],
27
+ stats: {
28
+ activeRuns: number,
29
+ sleepingRuns: number,
30
+ failed7d: number,
31
+ runVolume14d: number[],
32
+ },
33
+ createdAtMillis: number,
34
+ lastDeployedAtMillis: number,
35
+ };
36
+
37
+ export type AdminWorkflowRunState = "queued" | "running" | "sleeping" | "completed" | "failed" | "canceled";
38
+
39
+ export type AdminWorkflowDivergenceDiagnostic = {
40
+ reason: WorkflowDivergenceDiagnosticJson["reason"],
41
+ suspendedStepKey: string | null,
42
+ foundStepKey: string | null,
43
+ consumedStepKeys: string[],
44
+ unconsumedStepKeys: string[],
45
+ details: string,
46
+ };
47
+
48
+ export type AdminWorkflowRun = {
49
+ id: string,
50
+ workflowId: string,
51
+ runKey: string | null,
52
+ state: AdminWorkflowRunState,
53
+ version: number,
54
+ triggerType: string,
55
+ triggerSummary: string,
56
+ currentStepId: string | null,
57
+ stepsRecorded: number,
58
+ errorSummary: string | null,
59
+ failureKind: "user" | "platform" | null,
60
+ lastUpgradeDivergence: AdminWorkflowDivergenceDiagnostic | null,
61
+ createdAtMillis: number,
62
+ completedAtMillis: number | null,
63
+ nextWakeAtMillis: number | null,
64
+ };
65
+
66
+ export type AdminWorkflowStep = {
67
+ stepKey: string,
68
+ stepId: string,
69
+ kind: "run" | "sleep",
70
+ result: unknown,
71
+ resultSizeBytes: number,
72
+ attempts: number,
73
+ executedAtVersion: number,
74
+ elapsedMs: number | null,
75
+ createdAtMillis: number,
76
+ };
77
+
78
+ export type AdminWorkflowStepAttempt = {
79
+ stepKey: string,
80
+ stepId: string,
81
+ /** 0 for the original execution; incremented by each manual retry of the run. */
82
+ retryEpoch: number,
83
+ attempt: number,
84
+ outcome: "succeeded" | "failed",
85
+ error: { name: string, message: string, stack?: string } | null,
86
+ failureKind: "user" | "platform" | null,
87
+ logs: string | null,
88
+ startedAtMillis: number,
89
+ finishedAtMillis: number,
90
+ };
91
+
92
+ export type AdminWorkflowRunDetails = AdminWorkflowRun & {
93
+ triggerPayload: unknown,
94
+ steps: AdminWorkflowStep[],
95
+ stepAttempts: AdminWorkflowStepAttempt[],
96
+ };
97
+
98
+ export type AdminWorkflowVersion = {
99
+ workflowId: string,
100
+ version: number,
101
+ sourceHash: string,
102
+ source: string,
103
+ runtimeEnvVersion: string,
104
+ isLatest: boolean,
105
+ inFlightRuns: number,
106
+ createdAtMillis: number,
107
+ };
108
+
109
+ export type AdminWorkflowSyncResult = {
110
+ workflowId: string,
111
+ version: number,
112
+ created: boolean,
113
+ inFlightRunsOnOlderVersions: number,
114
+ };
115
+
116
+ export type AdminWorkflowUpgradeResult = {
117
+ upgradedCount: number,
118
+ skipped: {
119
+ runId: string,
120
+ runKey: string | null,
121
+ fromVersion: number,
122
+ diagnostic: AdminWorkflowDivergenceDiagnostic,
123
+ }[],
124
+ };
125
+
126
+ export type AdminWorkflowRunsFilter = {
127
+ state?: AdminWorkflowRunState,
128
+ version?: number,
129
+ runKey?: string,
130
+ cursor?: string,
131
+ limit?: number,
132
+ /** Include each run's memoized steps and attempt history. */
133
+ includeState?: boolean,
134
+ };
135
+
136
+ export function adminWorkflowFromCrud(crud: WorkflowSummaryJson): AdminWorkflow {
137
+ return {
138
+ id: crud.id,
139
+ displayName: crud.display_name,
140
+ latestVersion: crud.latest_version,
141
+ triggers: crud.triggers.map((trigger) => trigger.type === "event"
142
+ ? { type: "event", eventType: trigger.event_type }
143
+ : { type: "schedule", cron: trigger.cron, timezone: trigger.timezone }),
144
+ stats: {
145
+ activeRuns: crud.stats.active_runs,
146
+ sleepingRuns: crud.stats.sleeping_runs,
147
+ failed7d: crud.stats.failed_7d,
148
+ runVolume14d: crud.stats.run_volume_14d,
149
+ },
150
+ createdAtMillis: crud.created_at_millis,
151
+ lastDeployedAtMillis: crud.last_deployed_at_millis,
152
+ };
153
+ }
154
+
155
+ function adminDivergenceFromCrud(crud: WorkflowDivergenceDiagnosticJson | null): AdminWorkflowDivergenceDiagnostic | null {
156
+ if (crud == null) return null;
157
+ return {
158
+ reason: crud.reason,
159
+ suspendedStepKey: crud.suspended_step_key,
160
+ foundStepKey: crud.found_step_key,
161
+ consumedStepKeys: crud.consumed_step_keys,
162
+ unconsumedStepKeys: crud.unconsumed_step_keys,
163
+ details: crud.details,
164
+ };
165
+ }
166
+
167
+ export function adminWorkflowRunFromCrud(crud: WorkflowRunJson): AdminWorkflowRun {
168
+ return {
169
+ id: crud.id,
170
+ workflowId: crud.workflow_id,
171
+ runKey: crud.run_key,
172
+ state: crud.state,
173
+ version: crud.version,
174
+ triggerType: crud.trigger_type,
175
+ triggerSummary: crud.trigger_summary,
176
+ currentStepId: crud.current_step_id,
177
+ stepsRecorded: crud.steps_recorded,
178
+ errorSummary: crud.error_summary,
179
+ failureKind: crud.failure_kind,
180
+ lastUpgradeDivergence: adminDivergenceFromCrud(crud.last_upgrade_divergence),
181
+ createdAtMillis: crud.created_at_millis,
182
+ completedAtMillis: crud.completed_at_millis,
183
+ nextWakeAtMillis: crud.next_wake_at_millis,
184
+ };
185
+ }
186
+
187
+ export function isWorkflowRunDetailsJson(crud: WorkflowRunJson): crud is WorkflowRunDetailsJson {
188
+ return "trigger_payload" in crud
189
+ && "steps" in crud
190
+ && Array.isArray(crud.steps)
191
+ && "step_attempts" in crud
192
+ && Array.isArray(crud.step_attempts);
193
+ }
194
+
195
+ export function adminWorkflowRunDetailsFromCrud(crud: WorkflowRunDetailsJson): AdminWorkflowRunDetails {
196
+ return {
197
+ ...adminWorkflowRunFromCrud(crud),
198
+ triggerPayload: crud.trigger_payload,
199
+ steps: crud.steps.map((step) => ({
200
+ stepKey: step.step_key,
201
+ stepId: step.step_id,
202
+ kind: step.kind,
203
+ result: step.result,
204
+ resultSizeBytes: step.result_size_bytes,
205
+ attempts: step.attempts,
206
+ executedAtVersion: step.executed_at_version,
207
+ elapsedMs: step.elapsed_ms,
208
+ createdAtMillis: step.created_at_millis,
209
+ })),
210
+ stepAttempts: crud.step_attempts.map((attempt) => ({
211
+ stepKey: attempt.step_key,
212
+ stepId: attempt.step_id,
213
+ retryEpoch: attempt.retry_epoch,
214
+ attempt: attempt.attempt,
215
+ outcome: attempt.outcome,
216
+ error: attempt.error,
217
+ failureKind: attempt.failure_kind,
218
+ logs: attempt.logs,
219
+ startedAtMillis: attempt.started_at_millis,
220
+ finishedAtMillis: attempt.finished_at_millis,
221
+ })),
222
+ };
223
+ }
224
+
225
+ export function adminWorkflowVersionFromCrud(crud: WorkflowVersionJson): AdminWorkflowVersion {
226
+ return {
227
+ workflowId: crud.workflow_id,
228
+ version: crud.version,
229
+ sourceHash: crud.source_hash,
230
+ source: crud.source,
231
+ runtimeEnvVersion: crud.runtime_env_version,
232
+ isLatest: crud.is_latest,
233
+ inFlightRuns: crud.in_flight_runs,
234
+ createdAtMillis: crud.created_at_millis,
235
+ };
236
+ }
237
+
238
+ export function adminWorkflowSyncResultFromCrud(crud: WorkflowSyncResultJson): AdminWorkflowSyncResult {
239
+ return {
240
+ workflowId: crud.workflow_id,
241
+ version: crud.version,
242
+ created: crud.created,
243
+ inFlightRunsOnOlderVersions: crud.in_flight_runs_on_older_versions,
244
+ };
245
+ }