openxiangda 1.0.164 → 1.0.166

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 (33) hide show
  1. package/README.md +9 -7
  2. package/lib/change-baseline.js +74 -6
  3. package/lib/cli.js +2508 -167
  4. package/lib/sdd.js +320 -6
  5. package/lib/utils.js +76 -2
  6. package/openxiangda-skills/SKILL.md +13 -6
  7. package/openxiangda-skills/references/pages/publish-flow.md +15 -16
  8. package/openxiangda-skills/references/resource-manifest-cheatsheet.md +24 -2
  9. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +14 -6
  10. package/openxiangda-skills/skills/openxiangda-form/SKILL.md +10 -9
  11. package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +2 -0
  12. package/package.json +2 -1
  13. package/packages/sdk/dist/runtime/index.cjs +164 -48
  14. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  15. package/packages/sdk/dist/runtime/index.d.mts +1 -1
  16. package/packages/sdk/dist/runtime/index.d.ts +1 -1
  17. package/packages/sdk/dist/runtime/index.mjs +164 -48
  18. package/packages/sdk/dist/runtime/index.mjs.map +1 -1
  19. package/packages/sdk/dist/runtime/react.cjs +164 -48
  20. package/packages/sdk/dist/runtime/react.cjs.map +1 -1
  21. package/packages/sdk/dist/runtime/react.d.mts +79 -1
  22. package/packages/sdk/dist/runtime/react.d.ts +79 -1
  23. package/packages/sdk/dist/runtime/react.mjs +164 -48
  24. package/packages/sdk/dist/runtime/react.mjs.map +1 -1
  25. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +1 -0
  26. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +1 -0
  27. package/templates/openxiangda-react-spa/AGENTS.md +12 -10
  28. package/templates/openxiangda-react-spa/scripts/build-js-code.mjs +20 -2
  29. package/templates/openxiangda-react-spa/scripts/deploy.mjs +2 -2
  30. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +2 -0
  31. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +1 -0
  32. package/templates/sy-lowcode-app-workspace/AGENTS.md +5 -3
  33. package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +20 -2
@@ -572,13 +572,18 @@ JS_CODE 调用:`ctx.notification.sendByType({ ... })`。允许的 channels:`
572
572
  {
573
573
  "code": "reservation_reminder_summary",
574
574
  "name": "Reservation Reminder Summary",
575
+ "secretRefs": [
576
+ { "name": "notification_provider_token", "required": false }
577
+ ],
575
578
  "resources": {
576
579
  "forms": ["reservation_order"],
577
580
  "dataViews": ["reservation_order_overview"],
578
581
  "connectors": ["crm"]
579
582
  },
580
583
  "definitionJson": {
584
+ "version": "function_v2",
581
585
  "runtimeMode": "trusted_node",
586
+ "runtimeContractVersion": "trusted_node_v2",
582
587
  "timeout": 30000,
583
588
  "runtimeInvoke": {
584
589
  "audience": {
@@ -595,12 +600,19 @@ JS_CODE 调用:`ctx.notification.sendByType({ ... })`。允许的 channels:`
595
600
 
596
601
  ```ts
597
602
  // src/functions/reservation_reminder_summary/index.ts
598
- import type { AppFunctionContext } from "openxiangda/runtime";
603
+ import type { AppFunctionContextV2 } from "openxiangda/runtime";
599
604
 
600
605
  export default async function reservationReminderSummary(
601
- ctx: AppFunctionContext,
606
+ ctx: AppFunctionContextV2,
602
607
  input: { scope?: string },
603
608
  ) {
609
+ // Only resolve a name declared in top-level secretRefs. Never log or return it.
610
+ const providerToken = await ctx.secrets.get("notification_provider_token");
611
+ const providerResponse = await ctx.utils.http.post(
612
+ "https://api.dingtalk.com/v1.0/example/operation",
613
+ { requestId: input.scope || "default" },
614
+ { headers: { authorization: `Bearer ${providerToken}` } },
615
+ );
604
616
  const roleCodes = ctx.operator?.roleCodes || ctx.permissions?.roleCodes || [];
605
617
  const canManage =
606
618
  ctx.operator?.hasFullAccess === true ||
@@ -626,6 +638,7 @@ export default async function reservationReminderSummary(
626
638
  });
627
639
 
628
640
  return {
641
+ providerStatus: providerResponse.status,
629
642
  pendingCount: orders.totalCount || orders.data?.length || 0,
630
643
  detail,
631
644
  overview: overview.data || [],
@@ -634,6 +647,15 @@ export default async function reservationReminderSummary(
634
647
  }
635
648
  ```
636
649
 
650
+ `ctx.utils.http` is the typed, server-controlled public HTTPS bridge for
651
+ `trusted_node_v2`; it never forwards the platform Runtime bearer token and
652
+ rejects redirects plus private/reserved network targets. For whole-app atomic
653
+ activation, stage the exact backend scope with
654
+ `openxiangda resource publish function --only <code> --stage-only`, combine its
655
+ verified `stagedResource` with the other changed staged children, and pass that
656
+ overlay to `openxiangda release app-finalize --staged-resources-json ...`.
657
+ Already-active Backend Releases return `activeResource`, not a staged descriptor.
658
+
637
659
  调用方式:
638
660
 
639
661
  ```ts
@@ -26,6 +26,8 @@ Run `openxiangda update check --json` once per substantial task, on a suspected
26
26
 
27
27
  Every write/release command must include `--profile <name>`. In a shared checkout, create an isolated Git worktree and named feature branch before editing. `sdd propose|quick|context` claims `.openxiangda/worktree-owner.json` for the current `CODEX_THREAD_ID`; a foreign owner is a hard stop. `.openxiangda/state.json` maps profile-local IDs; it is not a collaboration lock. The task is not complete until the exact frozen publish SHA is merged without squash/rebase and pushed to the authoritative default branch.
28
28
 
29
+ SDD is permissive during coding and strict at release: `sdd verify --stage implementation` does not require unfinished checklist/evidence prose, and an omitted stage means prepublish rather than postpublish/archive. Exact command, child scope, and mainline gates apply only when staging or activating writes.
30
+
29
31
  First-class direct configuration writes (form/page/menu/role/permission/workflow/connector/notification/auth/route/public-access/scope/data-view/storage/settings) also require `--change <id>` or a complete stored session created by `openxiangda release begin --change <id>`. Missing context fails locally with `PUBLISH_CONTEXT_REQUIRED` before any HTTP write. Dry-run, schema planning, validation, query/invoke/export/submit/execution/upload, and other data-plane actions do not acquire a lease. Direct Workflow publish/unpublish/delete additionally requires the frozen `--expected-revision`; never fetch the latest revision merely to replay an older payload.
30
32
 
31
33
  ## Exact planning
@@ -55,7 +57,13 @@ openxiangda resource publish function --only function_a,function_b --profile <na
55
57
 
56
58
  Selectors apply before manifest parsing, source dependency analysis, and JS_CODE typecheck/build. A scoped Function/Automation plan must touch only the selected targets plus their transitive/shared/ambient dependencies; an unscoped plan intentionally retains full-workspace behavior. Resource commands use the canonical scoped builder packaged with the installed CLI for standard workspaces, so old checked-in builders still receive the optimization; refresh/bootstrap the workspace script only for equivalent manual `pnpm build-js-code` behavior. Nonstandard custom builders remain an explicit compatibility fallback.
57
59
 
58
- Source-triggered Function/Automation targets use one immutable Backend Release by default. The CLI uploads every eligible existing source-only target, freezes the current Backend Release parent plus Git/change baseline, then runs `prepare -> verify -> activate`; activation first CAS-checks the entire set and applies all updates in one transaction. Never fall back to sequential PATCH after prepare/verify/activate has started. Compatibility fallback is allowed only when the Backend Release `head` feature probe is an explicit HTTP 404, and must warn. Existing online bindings, contracts, metadata, trigger/view configuration, and enabled/published state remain unchanged; noops do not advance resource versions/timestamps. A deliberate whole-definition replacement requires exact `--only/--code` and `--replace-manifest --reason "<why>"`; SDD bypass does not imply replacement authority.
60
+ Source-triggered Function/Automation targets use Backend Release v2 when the platform exposes that capability. One child may mix create, source-only update, and manifest replacement update through explicit per-resource `operation/mode`; the CLI freezes the current Backend Release parent plus Git/change baseline, then runs `prepare -> verify -> activate` or stops verified for `--stage-only`. Activation CAS-checks the entire set and applies all updates in one transaction. `--stage-only` and Secret-bound Function publishing fail closed when Backend Release v2 is unavailable; compatibility fallback is limited to non-staged, non-Secret publishing after an explicit Backend head 404. Existing online bindings, contracts, metadata, trigger/view configuration, and enabled/published state remain unchanged; noops do not advance versions/timestamps. A deliberate whole-definition replacement requires exact `--only/--code` and `--replace-manifest --reason "<why>"`; SDD bypass does not imply replacement authority.
61
+
62
+ Functions with a top-level `secretRefs` field use `backend_release_v2`, including an explicit empty list that removes bindings. This path never falls back to source PATCH. It requires the per-app Secret capability probe to grant `app_function_secrets_v1`, `trusted_node_v2`, `backend_release_v2`, and `atomic_staged_children_v2`; otherwise plan/publish fails closed.
63
+
64
+ For whole-app atomic activation, publish the exact mixed Function/Automation scope with `resource publish function,automation --only function:<code>,automation:<code> --stage-only --change <change>`. This stops after Backend Release verify and returns `stagedResource` plus every handled selector; the CLI automatically merges it with other changed staged children in `.openxiangda/releases/<change>/staged-resources.json` for `release app-finalize --staged-resources-json`. A Backend Release that was already activated returns `activeResource` and must not be presented as staged.
65
+
66
+ Standard workspaces use the CLI-bundled scoped JS_CODE builder directly, including isolated Git-base checks; do not invoke `pnpm` merely to rebuild a selected Function/Automation. New snapshots carry `source_lineage_v1.sourceHash`, which is derived from authored source/dependencies rather than generated bundle bytes, so a builder upgrade does not falsely report source divergence.
59
67
 
60
68
  Use `openxiangda release backend-head|backend-list|backend-detail|backend-diff` to inspect immutable history. `backend-rollback <releaseId> --change <change> --reason "..."` prepares, verifies, and activates a new release from the historical snapshot; it never mutates or directly reactivates the old row. `backend-abort` stops prepared/verified work and `backend-retry` retries only recorded post-commit side effects. Do not fetch newer revisions to replay an older payload.
61
69
 
@@ -76,16 +84,16 @@ Do not call `lowcode-workspace publish-all`, `pnpm publish:all`, `pnpm publish:o
76
84
 
77
85
  ## React SPA
78
86
 
79
- Forms still publish schema shells explicitly. Resources and frontend runtime are separate releases:
87
+ Forms, Backend resources, and frontend Runtime are staged children of one Root App release:
80
88
 
81
89
  ```bash
82
- openxiangda workspace publish --profile <name> --form <formCode>
90
+ openxiangda resource publish form-setting --only <formCode> --change <change> --profile <name>
83
91
  openxiangda resource plan <type> --only <codes> --profile <name>
84
92
  openxiangda runtime deploy --change <change> --profile <name> --no-activate --json
85
- openxiangda sdd verify <change> --changed --stage prepublish
93
+ openxiangda sdd verify <change> --changed --stage prepublish --profile <name>
86
94
  openxiangda release begin --change <change> --profile <name>
87
- openxiangda resource publish <type> --only <codes> --change <change> --profile <name>
88
- openxiangda runtime activate <reviewedReleaseId> --change <change> --profile <name>
95
+ openxiangda resource publish function,automation --only function:<code>,automation:<code> --stage-only --change <change> --profile <name>
96
+ openxiangda release app-finalize --staged-resources-json .openxiangda/releases/<change>/staged-resources.json --change <change> --profile <name>
89
97
  # Merge/fast-forward the frozen release SHA to the remote default branch.
90
98
  openxiangda release integration-status --profile <name>
91
99
  openxiangda release end --profile <name>
@@ -23,9 +23,10 @@ openxiangda form list --profile <name>
23
23
  # src/forms/<formCode>/schema.ts — fields, options, rules, top-level FormEffect[]
24
24
  # src/forms/<formCode>/page.tsx — presentation only
25
25
 
26
- # 3. preview + publish single form
27
- openxiangda workspace publish --profile <name> --form <formCode> --dry-run
28
- openxiangda workspace publish --profile <name> --form <formCode>
26
+ # 3. normal release: stage the exact Form bundle, then Root App finalize
27
+ openxiangda resource plan form-setting --only <formCode> --profile <name>
28
+ openxiangda resource publish form-setting --only <formCode> --change <change> --profile <name>
29
+ openxiangda release app-finalize --staged-resources-json .openxiangda/releases/<change>/staged-resources.json --change <change> --profile <name>
29
30
  ```
30
31
 
31
32
  ## DO / DO NOT
@@ -71,19 +72,19 @@ src/forms/<formCode>/
71
72
  └── page.tsx
72
73
  ```
73
74
 
74
- Then publish:
75
+ Then stage the exact Form bundle:
75
76
 
76
77
  ```bash
77
- openxiangda workspace publish --profile <name>
78
+ openxiangda resource publish form-setting --only <formCodes> --change <change> --profile <name>
78
79
  ```
79
80
 
80
- For a single form edit, publish that form only:
81
+ For a single form edit, still keep the child staged until Root App finalize:
81
82
 
82
83
  ```bash
83
- openxiangda workspace publish --profile <name> --form <formCode>
84
+ openxiangda resource publish form-setting --only <formCode> --change <change> --profile <name>
84
85
  ```
85
86
 
86
- The workspace publish script receives `OPENXIANGDA_BASE_URL`, `OPENXIANGDA_ACCESS_TOKEN`, and `OPENXIANGDA_APP_TYPE` from the CLI. It creates/binds the platform form shell only when needed, syncs form metadata, builds the React form page bundle, uploads assets, and registers the bundle.
87
+ The Form resource bundle carries schema, settings, indexes, data-management, runtime-write, and public-access configuration under one CAS parent. It returns a canonical staged FormRelease and the CLI records it in the change-scoped staged-resources file. `workspace publish --form` is limited to an intentional first-time bootstrap or isolated repair outside a governed multi-resource release; it is not the normal release path.
87
88
 
88
89
  For Phase 6 React SPA workspaces, `app-workspace.config.ts` should declare
89
90
  `runtimeMode: "react-spa"`. In that mode, `workspace publish --form <code>` is
@@ -109,7 +110,7 @@ Do not use `openxiangda form create` as the normal way to generate a user-facing
109
110
  ## Form Release / CAS Safety
110
111
 
111
112
  - Treat all Form configuration as one immutable release stream. Freeze `revision`, `etag`, and `activeFormReleaseHead` from one snapshot before writing.
112
- - Publish declared schema, packages, settings, indexes, data-management, runtime-write, and public-access configuration through one Form `bundle` mutation. Resource Form bundles are stage-only by default. For normal multi-resource work, add each changed `FormRelease` entry (stable identity is `formUuid`) to `--staged-resources-json` and let root `app-finalize` atomically switch all child heads plus the App head. Use direct `--activate` only for an intentional form-only repair.
113
+ - Publish declared schema, packages, settings, indexes, data-management, runtime-write, and public-access configuration through one `resource publish form-setting --only <codes> --change <change>` Form `bundle` mutation. Resource Form bundles are stage-only by default and return immutable `contentHash` plus a canonical `stagedResource` (stable identity is `formUuid`); the CLI merges each entry into `.openxiangda/releases/<change>/staged-resources.json`. Root `app-finalize` atomically switches all child heads plus the App head. Use direct `--activate` only for an intentional form-only repair outside a governed multi-resource release; never use `workspace publish --form` inside an atomic React SPA release.
113
114
  - Every mutation must carry `If-Match`, `expectedRevision`, `expectedParent`, and a stable artifact ID. Creation starts at revision `0`.
114
115
  - A revision/head conflict is not retryable. Stop with zero later writes, re-plan, and explicitly merge the competing change; never silently adopt the newer live head.
115
116
  - Use `openxiangda form release-head|release-list|release-detail|release-diff` for inspection and `release-activate|release-rollback|release-abort` for controlled recovery. See `docs/openxiangda-form-releases.md` in the OpenXiangda tool repository.
@@ -105,6 +105,8 @@ JS_CODE is the backend execution escape hatch for workflow and automation. Use i
105
105
 
106
106
  App Function is the reusable backend execution model. Use it when the logic should be called by custom pages, multiple automations, workflows, or the runtime API. Source lives in `src/functions/<functionCode>/index.ts`; manifest lives in `src/resources/functions/<functionCode>.json`. Call it from pages with `sdk.function.invoke(code, { input })`, from graph definitions with `function_call`, or from the runtime endpoint `/:appType/v1/functions/:code/invoke.json`. Direct runtime invocation defaults to app automation management permission; ordinary page callers must declare `definitionJson.runtimeInvoke.audience` (`authenticated`, `page_permission_group`, `app_roles`, or `scope_policy`). Prefer `export default async function(ctx, input) {}` for App Function source; the second argument is the invoke input and the same value is available as `ctx.input`. Current MVP exposes controlled helpers only and does not expose raw SQL or Redis. `ctx.form.createOne/updateOne/updateById` are trusted backend writes governed by declared `resources.forms` and function invocation authorization; keep internal forms closed to direct user submit unless the business explicitly needs raw form submission. For internal function-only forms, set form settings `runtimeWrite.mode="function_only"` instead of relying only on missing submit permission groups.
107
107
 
108
+ For third-party credentials, use top-level metadata-only `secretRefs` plus `definitionJson.version="function_v2"` and `runtimeContractVersion="trusted_node_v2"`; read a declared value with `await ctx.secrets.get(name)` and call public business APIs through `ctx.utils.http`. Never use `process.env` for platform secrets. Values are managed with `openxiangda secret create|rotate --value-stdin --change <change> --profile <name>` and never appear in Git, build output, snapshots, plan diffs, logs, exceptions, or traces. Local tests may use `function test --secret-from-env logical=ENV` only; the value is passed to an isolated child over stdin and is never written to workspace/cache/state. For a Root App transaction, use exact-scope `resource publish function --only <code> --stage-only` and include its verified `stagedResource` in `release app-finalize`.
109
+
108
110
  For new AI-authored automations, prefer code-first `automation_code_ts` resources instead of visual v3 graph definitions. Put the source in `src/automations/<resourceCode>/index.ts`, define `definition.code.json` with `kind: "automation_code_ts"`, and provide `preview.json` for read-only frontend display. Use `ctx.logger.debug/info/warn/error(message, data?)` at every important step; OpenXiangda can inspect logs with `automation executions`, `automation logs`, and `automation diagnose`.
109
111
 
110
112
  For new AI-authored workflows where users do not need canvas editing, prefer compile-time `workflow.ts` using `openxiangda/workflow`. The CLI compiles it to v3 `definitionJson` and `preview.json`; the backend still runs the normal workflow engine for approval tasks, copy tasks, callback waits, branch advancement, and process records.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.164",
3
+ "version": "1.0.166",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {
@@ -76,6 +76,7 @@
76
76
  "test:api-contract": "node scripts/openxiangda-api-contract-smoke.mjs",
77
77
  "test:help-no-side-effects": "node scripts/help-no-side-effects-smoke.mjs",
78
78
  "test:app-function-fallback": "node scripts/app-function-source-fallback-smoke.mjs",
79
+ "test:app-function-secrets": "node scripts/app-function-secrets-cli-smoke.mjs",
79
80
  "test:change-baseline": "node scripts/change-baseline-smoke.mjs",
80
81
  "test:change-baseline-publish": "node scripts/change-baseline-publish-smoke.mjs",
81
82
  "test:generic-base-preflight": "node scripts/generic-git-base-preflight-smoke.mjs",
@@ -2411,6 +2411,8 @@ var resolveLoginUrl = (appType, {
2411
2411
  var getCurrentHref2 = () => typeof window === "undefined" ? "" : window.location.href;
2412
2412
 
2413
2413
  // packages/sdk/src/runtime/core/publicAccess.ts
2414
+ var PUBLIC_SESSION_RENEW_WINDOW_MS = 5 * 60 * 1e3;
2415
+ var publicSessionInflight = /* @__PURE__ */ new Map();
2414
2416
  var PublicAccessClientError = class extends Error {
2415
2417
  constructor(message7, options = {}) {
2416
2418
  super(message7);
@@ -2438,48 +2440,144 @@ var createPublicAccessClient = ({
2438
2440
  userAgent: input.userAgent || getCurrentUserAgent(),
2439
2441
  guestIdentifier: input.guestIdentifier || getOrCreatePublicGuestIdentifier(normalizedAppType)
2440
2442
  };
2441
- const response = await boundFetch(
2442
- buildServiceUrl2(
2443
- servicePrefix,
2444
- `/openxiangda-api/v1/apps/${encodeURIComponent(
2445
- normalizedAppType
2446
- )}/public/session`
2447
- ),
2448
- {
2449
- method: "POST",
2450
- credentials: "include",
2451
- headers: {
2452
- accept: "application/json",
2453
- "content-type": "application/json"
2454
- },
2455
- body: JSON.stringify(body)
2456
- }
2443
+ const identity = buildPublicSessionIdentity(
2444
+ normalizedAppType,
2445
+ servicePrefix,
2446
+ body
2457
2447
  );
2458
- const payload = await readPayload2(response);
2459
- const code = getRecordValue2(payload, "code");
2460
- const success = getRecordValue2(payload, "success");
2461
- if (!response.ok || success === false || !isSuccessCode3(code)) {
2462
- throw new PublicAccessClientError(
2463
- String(
2464
- getRecordValue2(payload, "message") || `Public access session failed: ${response.status}`
2448
+ const cached = readCachedPublicSession(identity);
2449
+ if (cached) return cached;
2450
+ const existing = publicSessionInflight.get(identity);
2451
+ if (existing) return existing;
2452
+ const pending = (async () => {
2453
+ const response = await boundFetch(
2454
+ buildServiceUrl2(
2455
+ servicePrefix,
2456
+ `/openxiangda-api/v1/apps/${encodeURIComponent(
2457
+ normalizedAppType
2458
+ )}/public/session`
2465
2459
  ),
2466
- { status: response.status, code, payload }
2460
+ {
2461
+ method: "POST",
2462
+ credentials: "include",
2463
+ headers: {
2464
+ accept: "application/json",
2465
+ "content-type": "application/json"
2466
+ },
2467
+ body: JSON.stringify(body)
2468
+ }
2467
2469
  );
2470
+ const payload = await readPayload2(response);
2471
+ const code = getRecordValue2(payload, "code");
2472
+ const success = getRecordValue2(payload, "success");
2473
+ if (!response.ok || success === false || !isSuccessCode3(code)) {
2474
+ throw new PublicAccessClientError(
2475
+ String(
2476
+ getRecordValue2(payload, "message") || `Public access session failed: ${response.status}`
2477
+ ),
2478
+ { status: response.status, code, payload }
2479
+ );
2480
+ }
2481
+ const data = getRecordValue2(payload, "data");
2482
+ const extra = getRecordValue2(payload, "extra");
2483
+ const session = {
2484
+ ...data,
2485
+ publicAccess: extra?.publicAccess || null,
2486
+ raw: payload
2487
+ };
2488
+ writeCachedPublicSession(identity, session);
2489
+ return session;
2490
+ })();
2491
+ publicSessionInflight.set(identity, pending);
2492
+ try {
2493
+ return await pending;
2494
+ } finally {
2495
+ if (publicSessionInflight.get(identity) === pending) {
2496
+ publicSessionInflight.delete(identity);
2497
+ }
2468
2498
  }
2469
- const data = getRecordValue2(payload, "data");
2470
- const extra = getRecordValue2(payload, "extra");
2471
- return {
2472
- ...data,
2473
- publicAccess: extra?.publicAccess || null,
2474
- raw: payload
2475
- };
2476
2499
  };
2477
2500
  return {
2478
2501
  appType: normalizedAppType,
2479
2502
  servicePrefix,
2480
- startSession: request
2503
+ startSession: request,
2504
+ clearSession: (input = {}) => {
2505
+ const body = {
2506
+ ...input,
2507
+ path: input.path || getCurrentPathname(),
2508
+ domain: input.domain || getCurrentDomain(),
2509
+ userAgent: input.userAgent || getCurrentUserAgent(),
2510
+ guestIdentifier: input.guestIdentifier || getOrCreatePublicGuestIdentifier(normalizedAppType)
2511
+ };
2512
+ clearCachedPublicSession(
2513
+ buildPublicSessionIdentity(normalizedAppType, servicePrefix, body)
2514
+ );
2515
+ }
2481
2516
  };
2482
2517
  };
2518
+ var buildPublicSessionIdentity = (appType, servicePrefix, input) => JSON.stringify({
2519
+ appType,
2520
+ servicePrefix: servicePrefix.replace(/\/$/, ""),
2521
+ path: input.path || "",
2522
+ policyCode: input.policyCode || "",
2523
+ routeCode: input.routeCode || "",
2524
+ ticket: input.ticket || "",
2525
+ guestIdentifier: input.guestIdentifier || "",
2526
+ domain: input.domain || ""
2527
+ });
2528
+ var publicSessionStorageKey = (identity) => {
2529
+ let hash = 2166136261;
2530
+ for (let index = 0; index < identity.length; index += 1) {
2531
+ hash ^= identity.charCodeAt(index);
2532
+ hash = Math.imul(hash, 16777619);
2533
+ }
2534
+ return `openxiangda:public:session:v1:${identity.length}:${(hash >>> 0).toString(16)}`;
2535
+ };
2536
+ var readCachedPublicSession = (identity) => {
2537
+ try {
2538
+ if (typeof window === "undefined" || !window.sessionStorage) return null;
2539
+ const raw = window.sessionStorage.getItem(publicSessionStorageKey(identity));
2540
+ if (!raw) return null;
2541
+ const parsed = JSON.parse(raw);
2542
+ if (parsed.identity !== identity || !parsed.session) return null;
2543
+ if (!isPublicSessionReusable(parsed.session)) {
2544
+ clearCachedPublicSession(identity);
2545
+ return null;
2546
+ }
2547
+ return parsed.session;
2548
+ } catch {
2549
+ return null;
2550
+ }
2551
+ };
2552
+ var writeCachedPublicSession = (identity, session) => {
2553
+ if (!isPublicSessionReusable(session)) return;
2554
+ try {
2555
+ if (typeof window === "undefined" || !window.sessionStorage) return;
2556
+ const { raw: _raw, ...cacheableSession } = session;
2557
+ window.sessionStorage.setItem(
2558
+ publicSessionStorageKey(identity),
2559
+ JSON.stringify({ identity, session: cacheableSession })
2560
+ );
2561
+ } catch {
2562
+ }
2563
+ };
2564
+ var clearCachedPublicSession = (identity) => {
2565
+ try {
2566
+ if (typeof window !== "undefined" && window.sessionStorage) {
2567
+ window.sessionStorage.removeItem(publicSessionStorageKey(identity));
2568
+ }
2569
+ } catch {
2570
+ }
2571
+ };
2572
+ var isPublicSessionReusable = (session) => {
2573
+ const accessTokenExpiresAt = Number(session.accessTokenExpiresAt);
2574
+ if (!Number.isFinite(accessTokenExpiresAt) || accessTokenExpiresAt <= 0) {
2575
+ return false;
2576
+ }
2577
+ const policyExpiresAt = session.publicAccess?.expiresAt ? new Date(session.publicAccess.expiresAt).getTime() : Number.POSITIVE_INFINITY;
2578
+ const expiresAt = Math.min(accessTokenExpiresAt, policyExpiresAt);
2579
+ return expiresAt - Date.now() >= PUBLIC_SESSION_RENEW_WINDOW_MS;
2580
+ };
2483
2581
  var buildServiceUrl2 = (servicePrefix, path) => {
2484
2582
  const prefix = servicePrefix.endsWith("/") ? servicePrefix.slice(0, -1) : servicePrefix;
2485
2583
  const suffix = path.startsWith("/") ? path : `/${path}`;
@@ -9915,29 +10013,40 @@ var usePublicAccess = (options = {}) => {
9915
10013
  setError(null);
9916
10014
  try {
9917
10015
  const resolvedPath = input.path || stableSessionInput.path || readPathFromLocation();
9918
- const data = await client.startSession({
10016
+ const resolvedInput = {
9919
10017
  ...stableSessionInput,
9920
10018
  ...input,
9921
10019
  ticket: input.ticket || stableSessionInput.ticket || readTicketFromLocation(),
9922
10020
  path: resolvedPath
9923
- });
9924
- if (!mountedRef.current) return data;
9925
- activeSessionRef.current = {
9926
- accessToken: data.accessToken,
9927
- path: resolvedPath
9928
10021
  };
9929
- setSession(data);
9930
- setAccessToken(data.accessToken, {
9931
- scope: "public",
9932
- path: resolvedPath
9933
- });
9934
- await reloadRuntime({
9935
- accessToken: data.accessToken,
9936
- accessTokenOptions: {
10022
+ let data = await client.startSession(resolvedInput);
10023
+ if (!mountedRef.current) return data;
10024
+ const activateSession = async (next) => {
10025
+ activeSessionRef.current = {
10026
+ accessToken: next.accessToken,
10027
+ path: resolvedPath
10028
+ };
10029
+ setSession(next);
10030
+ setAccessToken(next.accessToken, {
9937
10031
  scope: "public",
9938
10032
  path: resolvedPath
9939
- }
9940
- });
10033
+ });
10034
+ await reloadRuntime({
10035
+ accessToken: next.accessToken,
10036
+ accessTokenOptions: {
10037
+ scope: "public",
10038
+ path: resolvedPath
10039
+ }
10040
+ });
10041
+ };
10042
+ try {
10043
+ await activateSession(data);
10044
+ } catch (activationError) {
10045
+ if (!isUnauthorizedPublicSessionError(activationError)) throw activationError;
10046
+ client.clearSession(resolvedInput);
10047
+ data = await client.startSession(resolvedInput);
10048
+ await activateSession(data);
10049
+ }
9941
10050
  if (!mountedRef.current) return data;
9942
10051
  setLoading(false);
9943
10052
  return data;
@@ -9999,6 +10108,13 @@ var readTicketFromLocation = () => {
9999
10108
  return new URLSearchParams(window.location.search).get("ticket") || void 0;
10000
10109
  };
10001
10110
  var readPathFromLocation = () => typeof window === "undefined" ? void 0 : window.location.pathname;
10111
+ var isUnauthorizedPublicSessionError = (error) => {
10112
+ if (!error || typeof error !== "object") return false;
10113
+ const status = Number(
10114
+ error.status ?? error.statusCode
10115
+ );
10116
+ return status === 401 || status === 403;
10117
+ };
10002
10118
 
10003
10119
  // packages/sdk/src/runtime/host/builtinRouteRenderer.tsx
10004
10120
  var import_react88 = require("react");