@wayai/cli 0.3.221 → 0.3.222

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.
package/dist/index.js CHANGED
@@ -4604,6 +4604,38 @@ var init_zod = __esm({
4604
4604
  }
4605
4605
  });
4606
4606
 
4607
+ // ../../packages/core/dist/chunk-VXEAR36B.js
4608
+ var orgTagIdParam, listOrgTagsQuery, ORG_TAG_NAME_REGEX, ORG_TAG_NAME_MAX_LENGTH, createOrgTagBody, updateOrgTagBody;
4609
+ var init_chunk_VXEAR36B = __esm({
4610
+ "../../packages/core/dist/chunk-VXEAR36B.js"() {
4611
+ "use strict";
4612
+ init_zod();
4613
+ orgTagIdParam = external_exports.object({
4614
+ id: external_exports.string().uuid()
4615
+ });
4616
+ listOrgTagsQuery = external_exports.object({
4617
+ organization_id: external_exports.string().uuid()
4618
+ });
4619
+ ORG_TAG_NAME_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
4620
+ ORG_TAG_NAME_MAX_LENGTH = 50;
4621
+ createOrgTagBody = external_exports.object({
4622
+ organization_id: external_exports.string().uuid(),
4623
+ name: external_exports.string().min(1).max(ORG_TAG_NAME_MAX_LENGTH).regex(ORG_TAG_NAME_REGEX, {
4624
+ message: 'Must be lowercase alphanumeric with hyphens (e.g. "my-tag")'
4625
+ }),
4626
+ display_name: external_exports.string().max(100).nullish(),
4627
+ color: external_exports.string().max(20).nullish()
4628
+ });
4629
+ updateOrgTagBody = external_exports.object({
4630
+ // Required so the route targets the org the UI is managing (which may differ
4631
+ // from the token's default `auth.org_id` for multi-org users) — mirrors create/list.
4632
+ organization_id: external_exports.string().uuid(),
4633
+ display_name: external_exports.string().max(100).nullish(),
4634
+ color: external_exports.string().max(20).nullish()
4635
+ });
4636
+ }
4637
+ });
4638
+
4607
4639
  // ../../packages/core/dist/chunk-HZ6CADSY.js
4608
4640
  function optionalTrimmedText(max) {
4609
4641
  return external_exports.string().trim().max(max).optional().transform((value) => value ? value : void 0);
@@ -5251,34 +5283,21 @@ var init_chunk_FK6HBXM7 = __esm({
5251
5283
  }
5252
5284
  });
5253
5285
 
5254
- // ../../packages/core/dist/chunk-VXEAR36B.js
5255
- var orgTagIdParam, listOrgTagsQuery, ORG_TAG_NAME_REGEX, ORG_TAG_NAME_MAX_LENGTH, createOrgTagBody, updateOrgTagBody;
5256
- var init_chunk_VXEAR36B = __esm({
5257
- "../../packages/core/dist/chunk-VXEAR36B.js"() {
5286
+ // ../../packages/core/dist/chunk-GPM7PE44.js
5287
+ var bootstrapQuery;
5288
+ var init_chunk_GPM7PE44 = __esm({
5289
+ "../../packages/core/dist/chunk-GPM7PE44.js"() {
5258
5290
  "use strict";
5259
5291
  init_zod();
5260
- orgTagIdParam = external_exports.object({
5261
- id: external_exports.string().uuid()
5262
- });
5263
- listOrgTagsQuery = external_exports.object({
5264
- organization_id: external_exports.string().uuid()
5265
- });
5266
- ORG_TAG_NAME_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
5267
- ORG_TAG_NAME_MAX_LENGTH = 50;
5268
- createOrgTagBody = external_exports.object({
5269
- organization_id: external_exports.string().uuid(),
5270
- name: external_exports.string().min(1).max(ORG_TAG_NAME_MAX_LENGTH).regex(ORG_TAG_NAME_REGEX, {
5271
- message: 'Must be lowercase alphanumeric with hyphens (e.g. "my-tag")'
5272
- }),
5273
- display_name: external_exports.string().max(100).nullish(),
5274
- color: external_exports.string().max(20).nullish()
5275
- });
5276
- updateOrgTagBody = external_exports.object({
5277
- // Required so the route targets the org the UI is managing (which may differ
5278
- // from the token's default `auth.org_id` for multi-org users) — mirrors create/list.
5279
- organization_id: external_exports.string().uuid(),
5280
- display_name: external_exports.string().max(100).nullish(),
5281
- color: external_exports.string().max(20).nullish()
5292
+ bootstrapQuery = external_exports.object({
5293
+ active_hub_id: external_exports.string().uuid().optional(),
5294
+ active_conversation_id: external_exports.string().uuid().optional(),
5295
+ active_nav: external_exports.enum(["chat", "task", "support"]).optional(),
5296
+ /**
5297
+ * The people thread the URL names (`/chat/threads/:threadId`, gh #4661): the snapshot
5298
+ * preloads it as `people.active_thread` when the viewer is a member.
5299
+ */
5300
+ active_thread_id: external_exports.string().uuid().optional()
5282
5301
  });
5283
5302
  }
5284
5303
  });
@@ -5553,6 +5572,583 @@ var init_chunk_5DGHIY7L = __esm({
5553
5572
  }
5554
5573
  });
5555
5574
 
5575
+ // ../../packages/core/dist/chunk-F3JHI4Y4.js
5576
+ var DATA_PROXY_MOUNT, DATA_PROXY_PREFIX, BASE_DESTRUCTION_MOUNT, BASE_DESTRUCTION_PREFIX, BASE_DESTRUCTION_CONFIRM_PATH, COMMUNITY_EMAIL_UNSUBSCRIBE_PATH, CONTACT_BOOK_EMAIL_UNSUBSCRIBE_PATH, AUTOMATION_WEBHOOK_MOUNT;
5577
+ var init_chunk_F3JHI4Y4 = __esm({
5578
+ "../../packages/core/dist/chunk-F3JHI4Y4.js"() {
5579
+ "use strict";
5580
+ DATA_PROXY_MOUNT = "/data";
5581
+ DATA_PROXY_PREFIX = `/api${DATA_PROXY_MOUNT}`;
5582
+ BASE_DESTRUCTION_MOUNT = "/bases";
5583
+ BASE_DESTRUCTION_PREFIX = `/api${BASE_DESTRUCTION_MOUNT}`;
5584
+ BASE_DESTRUCTION_CONFIRM_PATH = `${BASE_DESTRUCTION_PREFIX}/destruction/confirm`;
5585
+ COMMUNITY_EMAIL_UNSUBSCRIBE_PATH = "/api/communities/email-unsubscribe";
5586
+ CONTACT_BOOK_EMAIL_UNSUBSCRIBE_PATH = "/api/contact-book/email-unsubscribe";
5587
+ AUTOMATION_WEBHOOK_MOUNT = "/webhooks/automations";
5588
+ }
5589
+ });
5590
+
5591
+ // ../../packages/core/dist/chunk-IEWJ5MZH.js
5592
+ function matchPath(pattern, path42) {
5593
+ const normalizedPath = path42.replace(/\/$/, "");
5594
+ if (pattern.endsWith("/*")) {
5595
+ const prefix = pattern.slice(0, -2);
5596
+ return normalizedPath === prefix || normalizedPath.startsWith(prefix + "/");
5597
+ }
5598
+ return pattern === normalizedPath;
5599
+ }
5600
+ function getEndpointConfig(path42) {
5601
+ for (const excluded of EXCLUDED_PATHS) {
5602
+ if (matchPath(excluded, path42)) {
5603
+ return null;
5604
+ }
5605
+ }
5606
+ const normalizedPath = path42.replace(/\/$/, "");
5607
+ if (!normalizedPath.endsWith("/*") && Object.prototype.hasOwnProperty.call(ENDPOINT_CONFIGS, normalizedPath)) {
5608
+ return ENDPOINT_CONFIGS[normalizedPath];
5609
+ }
5610
+ const sortedPatterns = Object.keys(ENDPOINT_CONFIGS).sort((a, b) => b.length - a.length);
5611
+ for (const pattern of sortedPatterns) {
5612
+ if (matchPath(pattern, path42)) {
5613
+ return ENDPOINT_CONFIGS[pattern];
5614
+ }
5615
+ }
5616
+ return { tier: "STANDARD" };
5617
+ }
5618
+ var ENDPOINT_CONFIGS, EXCLUDED_PATHS;
5619
+ var init_chunk_IEWJ5MZH = __esm({
5620
+ "../../packages/core/dist/chunk-IEWJ5MZH.js"() {
5621
+ "use strict";
5622
+ init_chunk_F3JHI4Y4();
5623
+ ENDPOINT_CONFIGS = {
5624
+ // OAuth callbacks - strict, fail-closed (prevent brute force)
5625
+ "/oauth/callback/*": { tier: "STRICT", failClosed: true },
5626
+ // Auth endpoints - strict, fail-closed
5627
+ "/auth/phone": { tier: "STRICT", failClosed: true },
5628
+ "/auth/mcp/exchange": { tier: "MODERATE", failClosed: true },
5629
+ // Native magic-code login (pre-auth) — STRICT per-IP bounds a single host; WorkOS owns
5630
+ // the per-email lockout / TTL / single-use guarantees.
5631
+ "/api/auth/magic-code/*": { tier: "STRICT", failClosed: true },
5632
+ // Native password login (pre-auth) — same posture as magic-code above. STRICT per-IP bounds a
5633
+ // single host but FAILS OPEN on a binding error (like every IP limit); the `failClosed` flag
5634
+ // here is inert on a pre-auth route (it's only enforced on the per-user branch, which needs an
5635
+ // authenticated caller). The fail-CLOSED brute-force guarantee for the static credential comes
5636
+ // from the route's per-email caps (burst + cumulative hourly), not this flag.
5637
+ "/api/auth/password/*": { tier: "STRICT", failClosed: true },
5638
+ // Every `/api/*` group whose paths carry ids pins `userLimitKey` (gh #4756), held there by
5639
+ // `security/endpoint-invariants.test.ts`.
5640
+ // Billing - moderate, fail-closed with user limits
5641
+ "/api/billing/*": {
5642
+ tier: "MODERATE",
5643
+ userLimit: 100,
5644
+ failClosed: true,
5645
+ skipAuthenticatedUsers: true,
5646
+ userLimitKey: "/api/billing/*"
5647
+ },
5648
+ "/webhooks/stripe": { tier: "MODERATE", failClosed: true },
5649
+ "/webhooks/workos": { tier: "MODERATE", failClosed: true },
5650
+ // API endpoints - standard with user limits
5651
+ // 500 because the web's settings live updates refetch per hub in every open tab, whatever page
5652
+ // it shows (`settingsLiveUpdateStore.ts`): one batch of changes costs about 10 + 4 per agent
5653
+ // whose tools were opened + 2 per loaded resource, so a CLI push and publish (both hubs) with
5654
+ // two tabs open passes 150 in a minute. A multi-file resource upload adds 2–5 per file.
5655
+ "/api/setup/*": {
5656
+ tier: "STANDARD",
5657
+ userLimit: 500,
5658
+ skipAuthenticatedUsers: true,
5659
+ userLimitKey: "/api/setup/*"
5660
+ },
5661
+ // 500 because an eval run's drill-down (`SessionResults.tsx`) fetches its list and then every
5662
+ // LLM turn's record at once: up to 201 requests (`OBSERVABILITY_LIST_CAP`, 200). Sending keeps
5663
+ // the 150 it had when each path held its own budget, on the exact key below.
5664
+ "/api/conversations/*": {
5665
+ tier: "STANDARD",
5666
+ userLimit: 500,
5667
+ skipAuthenticatedUsers: true,
5668
+ userLimitKey: "/api/conversations/*"
5669
+ },
5670
+ "/api/conversations/send-message": { tier: "STANDARD", userLimit: 150, skipAuthenticatedUsers: true },
5671
+ "/api/users/*": { tier: "STANDARD", userLimit: 150, skipAuthenticatedUsers: true },
5672
+ "/api/analytics/*": {
5673
+ tier: "STANDARD",
5674
+ userLimit: 150,
5675
+ skipAuthenticatedUsers: true,
5676
+ userLimitKey: "/api/analytics/*"
5677
+ },
5678
+ // 500 because `wayai eval-results --eval` reads completed sessions one by one, newest first,
5679
+ // until one holds the eval (up to the 1000 the list returns). Tenant SQL keeps the 150 it had
5680
+ // when each path held its own budget: each query may run 30 s on shared-tenant ClickHouse
5681
+ // after its caller has gone (`sql-validator.ts`, stage 10b).
5682
+ "/api/evals/*": {
5683
+ tier: "STANDARD",
5684
+ userLimit: 500,
5685
+ skipAuthenticatedUsers: true,
5686
+ userLimitKey: "/api/evals/*"
5687
+ },
5688
+ "/api/evals/sql": { tier: "STANDARD", userLimit: 150, skipAuthenticatedUsers: true },
5689
+ // An eval run's calls (gh #4418), kept off the end user's call budgets below. Create keeps the
5690
+ // 150 it had: it opens a provider session on the org's own key, billed per second. The routes
5691
+ // on one call take a budget of their own so no eval read can spend it: a refused hang-up
5692
+ // leaves the call running, and billing. 500 because a call suite polls each run's record
5693
+ // every second, about 64 requests a minute per suite.
5694
+ "/api/evals/calls": { tier: "STANDARD", userLimit: 150, skipAuthenticatedUsers: true },
5695
+ "/api/evals/calls/*": {
5696
+ tier: "STANDARD",
5697
+ userLimit: 500,
5698
+ skipAuthenticatedUsers: true,
5699
+ userLimitKey: "/api/evals/calls/*"
5700
+ },
5701
+ "/api/files/*": { tier: "STANDARD", userLimit: 150, skipAuthenticatedUsers: true },
5702
+ // The org contact book (wayai#4931 PR 2b). A CSV import sends one request per
5703
+ // `CONTACT_IMPORT_BATCH_MAX` rows, so 150 a minute imports 75,000 rows a minute.
5704
+ "/api/contact-book/*": {
5705
+ tier: "STANDARD",
5706
+ userLimit: 150,
5707
+ skipAuthenticatedUsers: true,
5708
+ userLimitKey: "/api/contact-book/*"
5709
+ },
5710
+ // An automation email's one-click unsubscribe (wayai#4931 PR 2d) — public: its token is its one
5711
+ // credential, so this per-IP limit is its one bound, and runs before the route reads anything.
5712
+ // MODERATE (30/min per IP), as the community email's unsubscribe, for the same reason: mail
5713
+ // clients' one-click unsubscribe POSTs it from their own servers, which many recipients' clicks
5714
+ // share. An exact key, so it wins over the wildcard above.
5715
+ [CONTACT_BOOK_EMAIL_UNSUBSCRIBE_PATH]: { tier: "MODERATE" },
5716
+ "/api/automations/*": {
5717
+ tier: "STANDARD",
5718
+ userLimit: 150,
5719
+ skipAuthenticatedUsers: true,
5720
+ userLimitKey: "/api/automations/*"
5721
+ },
5722
+ // A hub's own contact lists (wayai#4931 PR 2c): config edits, at the automations' rate.
5723
+ "/api/hub-contact-lists/*": {
5724
+ tier: "STANDARD",
5725
+ userLimit: 150,
5726
+ skipAuthenticatedUsers: true,
5727
+ userLimitKey: "/api/hub-contact-lists/*"
5728
+ },
5729
+ "/api/message-templates/*": {
5730
+ tier: "STANDARD",
5731
+ userLimit: 150,
5732
+ skipAuthenticatedUsers: true,
5733
+ userLimitKey: "/api/message-templates/*"
5734
+ },
5735
+ "/api/live-updates/*": { tier: "STANDARD", userLimit: 150, skipAuthenticatedUsers: true },
5736
+ "/api/hub-users/*": {
5737
+ tier: "STANDARD",
5738
+ userLimit: 150,
5739
+ skipAuthenticatedUsers: true,
5740
+ userLimitKey: "/api/hub-users/*"
5741
+ },
5742
+ "/api/conversation-state/*": {
5743
+ tier: "STANDARD",
5744
+ userLimit: 150,
5745
+ skipAuthenticatedUsers: true,
5746
+ userLimitKey: "/api/conversation-state/*"
5747
+ },
5748
+ // The authenticated byte route (`routes/files/r2.ts`), mounted after its `authMiddleware` so
5749
+ // the caller is known: each request reads the HubDO its key names, and a hub id never created
5750
+ // still wakes a Durable Object (gh #4747; since gh #4775 T5 one that writes nothing). The key is pinned
5751
+ // because the paths are file keys, which would otherwise each get a fresh budget (and one
5752
+ // over 256 characters would be refused outright, `checkUserLimitNative`).
5753
+ //
5754
+ // 500 a minute because the web chat fetches every attachment of every loaded message at
5755
+ // once, with no list virtualization: up to 50 per 50-message page, 200 per ended
5756
+ // conversation or history entry at one file per message, and 4 per message at most in its
5757
+ // image grid. Repeats come from the browser's HTTP cache (`private, max-age=3600`); mobile
5758
+ // renders through a disk cache and plays audio from `/files/signed`. The budget is shared by
5759
+ // a caller's tabs and devices. The other two byte routes, `/files/signed/*` and
5760
+ // `/files/public/*`, mount no limiter (the reason is at their entries in
5761
+ // `security/endpoint-invariants.test.ts`'s route registry).
5762
+ "/files/auth/*": {
5763
+ tier: "STANDARD",
5764
+ userLimit: 500,
5765
+ skipAuthenticatedUsers: true,
5766
+ userLimitKey: "/files/auth/*"
5767
+ },
5768
+ // Channel webhooks - high throughput (external systems, never authenticated)
5769
+ "/webhooks/whatsapp/*": { tier: "HIGH" },
5770
+ "/webhooks/instagram/*": { tier: "HIGH" },
5771
+ // An automation's webhook deliveries (wayai#4931 D13) — no bearer token: the automation's
5772
+ // secret is the credential, checked against one HubDO read. This per-IP limit runs first, before
5773
+ // anything is read. HIGH, as the channel webhooks: senders are servers, and an integration
5774
+ // platform's egress addresses carry many tenants' deliveries. The per-automation budget is
5775
+ // `AUTOMATION_WEBHOOK_PER_AUTOMATION_LIMIT`, counted after the delivery is verified.
5776
+ [`${AUTOMATION_WEBHOOK_MOUNT}/*`]: { tier: "HIGH" },
5777
+ // Reports (CLI → platform_report triage queue). One per-user budget covers intake
5778
+ // (POST), the content edit (PATCH /:id), the reporter read-back (GET, GET /:id), and the
5779
+ // verification loop (POST /:id/accept, /:id/contest). The limiter is path-based, so a
5780
+ // method-level split (loose reads, strict writes) isn't expressible — GET /:id and PATCH
5781
+ // /:id share a path. So the budget is sized for the read-heavy poll loop (a reporter agent
5782
+ // polling `report get`/`list` while it waits for the fix), at 60/hour. Intake-flooding stays
5783
+ // bounded regardless: identical reports collapse on `fingerprint` (ON CONFLICT DO NOTHING),
5784
+ // so a runaway create loop merges rather than floods. Still STRICT tier + failClosed so a
5785
+ // genuinely runaway distinct-report loop surfaces as 429. The `/*` is load-bearing — an
5786
+ // exact `/api/reports` key would leave the `/:id` paths unmatched (→ unthrottled).
5787
+ // `userLimitKey` is equally load-bearing: without it, middleware falls back to each request's
5788
+ // path and report IDs/actions receive independent counters instead of this shared budget.
5789
+ "/api/reports/*": {
5790
+ tier: "STRICT",
5791
+ userLimit: 60,
5792
+ userWindow: 3600,
5793
+ failClosed: true,
5794
+ skipAuthenticatedUsers: true,
5795
+ userLimitKey: "/api/reports/*"
5796
+ },
5797
+ // Admin report mutations (transition/group/ungroup/edit) — platform-admin
5798
+ // gated at the route, rate-limited here as defense-in-depth. Generous STANDARD
5799
+ // cap so legitimate triage bursts aren't throttled (grouping is one request
5800
+ // even for many members); deliberately NOT failClosed so a Redis blip never
5801
+ // blocks an admin's triage. The bare GET reads share this budget harmlessly.
5802
+ "/api/admin/reports/*": { tier: "STANDARD", userLimit: 150, userLimitKey: "/api/admin/reports/*" },
5803
+ // GitHub webhook (issues.closed → platform_report addressed). MODERATE +
5804
+ // fail-closed; GitHub retries on transient failures so a brief Redis blip
5805
+ // doesn't lose state.
5806
+ "/webhooks/github": { tier: "MODERATE", failClosed: true },
5807
+ // Account deletion — strict on the destructive POST + public undo. The
5808
+ // /api/users/* default tier above ALSO matches these, but `getEndpointConfig`
5809
+ // returns the longest-prefix match, so the more specific entries here win.
5810
+ // `skipAuthenticatedUsers` deliberately omitted: every caller is authenticated,
5811
+ // so including it would make `userLimit` a no-op — the per-user cap IS the gate.
5812
+ "/api/users/me/deletion": { tier: "STRICT", userLimit: 3, userWindow: 3600, failClosed: true },
5813
+ "/api/users/me/deletion/preflight": { tier: "STANDARD", userLimit: 30, failClosed: true },
5814
+ "/api/users/deletion/undo": { tier: "STRICT", failClosed: true },
5815
+ // Production-base destruction.
5816
+ //
5817
+ // Keyed off the CONTRACT constants, not literals: `BASE_DESTRUCTION_CONFIRM_PATH`'s own
5818
+ // docstring promises that the mount, the `AUTH_BYPASS_PATHS` entry, the rate-limit key and
5819
+ // the URL the email embeds move together, and a literal here is exactly how that promise
5820
+ // goes quietly false — leaving an unauthenticated HMAC endpoint on the default tier.
5821
+ //
5822
+ // The emailed GET is PUBLIC, so it gets the IP-tier bound the pre-auth routes get.
5823
+ // `failClosed` is deliberately NOT set: it is only consulted on the per-USER branch, which
5824
+ // needs an authenticated caller, so on a pre-auth route it is inert — the same thing
5825
+ // `/api/auth/magic-code/send` above says about itself. The IP limiter fails OPEN on a
5826
+ // limiter error by design (`packages/runtime/src/rate-limiter.ts`), and claiming otherwise
5827
+ // would tell the next auditor a protection exists that does not.
5828
+ [BASE_DESTRUCTION_CONFIRM_PATH]: { tier: "STRICT" },
5829
+ // The console's read, which every production base's settings pane issues on mount. Without
5830
+ // its own entry it inherits the mount-wide budget below, and an admin clicking through
5831
+ // thirty bases in an hour would be 429'd by a limit sized for irreversible writes.
5832
+ [`${BASE_DESTRUCTION_PREFIX}/destructions`]: { tier: "STANDARD", userLimit: 150 },
5833
+ // The mutating routes: initiate, the authenticated confirm, and cancel. Per user rather than
5834
+ // per IP — all three are authenticated and org-admin gated — and hourly, since a single human
5835
+ // touching thirty production-deletion operations in an hour is already far past normal.
5836
+ // Longest-prefix wins, so the two entries above are not widened by this one. The key is
5837
+ // pinned so those thirty are counted across every base the caller touches (gh #4756).
5838
+ [`${BASE_DESTRUCTION_PREFIX}/*`]: {
5839
+ tier: "STANDARD",
5840
+ userLimit: 30,
5841
+ userWindow: 3600,
5842
+ failClosed: true,
5843
+ userLimitKey: `${BASE_DESTRUCTION_PREFIX}/*`
5844
+ },
5845
+ // Email-invite preview — public, fail-closed. MODERATE (30/min IP) leaves
5846
+ // headroom for the signup-page server-fetch + reloads + prefetches; STRICT
5847
+ // would throttle legitimate users opening the link in multiple tabs.
5848
+ "/api/invites/preview": { tier: "MODERATE", failClosed: true },
5849
+ // "Don't email me again" (gh #4661) — public; one POST per click on its confirm page. STRICT
5850
+ // per IP. No `failClosed`: it is consulted only on the per-user branch, which a pre-auth route
5851
+ // never reaches (see `BASE_DESTRUCTION_CONFIRM_PATH` above).
5852
+ "/api/invites/unsubscribe": { tier: "STRICT" },
5853
+ // Admin DO debug reads — the ONE deliberate deviation from the Data Explorer
5854
+ // catch-all below, and the only reason a per-route key still exists here.
5855
+ // Platform-admin is enforced at the route; this is the independent
5856
+ // defense-in-depth signal so a runaway investigation script surfaces as 429.
5857
+ //
5858
+ // STRICT is kept ON PURPOSE even though the catch-all moved off it: per IP, at 5/min vs
5859
+ // STANDARD's 100/min, walking 10k ids takes ~33h instead of ~100min — a detection window
5860
+ // worth keeping on the routes that return RAW DO ROWS.
5861
+ //
5862
+ // The cost, stated rather than elided: STRICT is one bucket shared with
5863
+ // `/oauth/callback/*` and `/api/auth/magic-code/*`, so a debug session from a
5864
+ // NAT'd office IP can 429 a colleague's login for up to a minute. That trade is
5865
+ // accepted for a rare, deliberate investigation surface — and is exactly why the
5866
+ // browse routes below are NOT here.
5867
+ //
5868
+ // The key is pinned, so a sweep of `/debug/do/hub/<id>/tables` across ids spends one
5869
+ // per-admin budget (gh #4756). 120 an hour: an agent's investigation of a dozen
5870
+ // conversations made 37 debug reads in its busiest hour.
5871
+ "/api/admin/data-explorer/debug/*": {
5872
+ tier: "STRICT",
5873
+ userLimit: 120,
5874
+ userWindow: 3600,
5875
+ failClosed: true,
5876
+ userLimitKey: "/api/admin/data-explorer/debug/*"
5877
+ },
5878
+ // Catch-all for the rest of the Data Explorer (`/search`, `/entity/*`, `/do/*`,
5879
+ // `/kv/*`, the DELETE routes). Added as a catch-all rather than another per-route
5880
+ // key because this budget kept lagging behind routes joining the debug boundary:
5881
+ // with no match the path falls to the default, which sets no `userLimit`, so the
5882
+ // per-user layer never runs at all. `getEndpointConfig` takes the longest match,
5883
+ // so `debug/*` above still wins.
5884
+ //
5885
+ // `/entity/*` deliberately has NO key of its own: it needs precisely this config,
5886
+ // and a duplicate entry is how the two drift apart on the next edit.
5887
+ //
5888
+ // `tier` is deliberately STANDARD, not STRICT. `tier` is the per-IP knob and
5889
+ // STRICT is 5/min keyed on IP ALONE — one bucket shared across every STRICT
5890
+ // endpoint. On these browse routes that breaks the console (open Browse KV and
5891
+ // inspect four values = 6 requests; expand six org rows in the search results =
5892
+ // 6 `/entity/org/:id` calls) and would drain the same IP's budget for
5893
+ // `/oauth/callback/*` and `/api/auth/magic-code/*`, which on a NAT'd office IP
5894
+ // means all admins. The per-user hourly cap is the leg that matters here.
5895
+ //
5896
+ // The canonical key is deliberate: request paths contain DO ids and arbitrary
5897
+ // malformed requests can contain credential material. Keying on the request path
5898
+ // both stores that material in Redis/logs and gives every target a fresh counter.
5899
+ // One stable identity makes every browse request consume the same per-admin
5900
+ // budget without storing request targets.
5901
+ //
5902
+ // 600/hour preserves the original pagination requirement (about 30k rows at
5903
+ // 50/page) while now genuinely bounding id/key enumeration across the surface.
5904
+ "/api/admin/data-explorer/*": {
5905
+ tier: "STANDARD",
5906
+ userLimit: 600,
5907
+ userWindow: 3600,
5908
+ failClosed: true,
5909
+ userLimitKey: "/api/admin/data-explorer/*"
5910
+ },
5911
+ // Admin harness incident-response (harness-agents PR 2.9b) — mass-destroy reaps
5912
+ // EVERY live harness session/token/slot in scope, so it gets a strict budget
5913
+ // (STRICT + a low per-user hourly cap + fail-closed). Platform-admin is enforced
5914
+ // at the route; this is the independent defense-in-depth signal so a runaway
5915
+ // reap loop surfaces as 429. The break-glass disable/enable rides through
5916
+ // `/api/admin/config` (its own STANDARD budget), not this key.
5917
+ "/api/admin/harness/*": {
5918
+ tier: "STRICT",
5919
+ userLimit: 10,
5920
+ userWindow: 3600,
5921
+ failClosed: true,
5922
+ userLimitKey: "/api/admin/harness/*"
5923
+ },
5924
+ // Platform-wide team-orphan repair — a single call can WRITE across every hub of
5925
+ // every org (HubDO purge + UserDO mirror fan-out), so it gets the same strict,
5926
+ // fail-closed budget as the other cross-platform admin write. Exact path (not a
5927
+ // prefix): the sibling `/api/admin/hubs/:hubId/harness` flag flips stay on the
5928
+ // default admin budget. Platform-admin is enforced at the route; this is the
5929
+ // defense-in-depth signal that a repair loop surfaces as 429.
5930
+ "/api/admin/hubs/repair-team-orphans": { tier: "STRICT", userLimit: 10, userWindow: 3600, failClosed: true },
5931
+ // Branching-orphan repair (#4577) — deletes named production rows, and its report mode
5932
+ // fans out over every production hub of every org, so it takes the same strict,
5933
+ // fail-closed budget as the team-orphan repair. Exact path, for the same reason.
5934
+ "/api/admin/hubs/repair-branching-orphans": { tier: "STRICT", userLimit: 10, userWindow: 3600, failClosed: true },
5935
+ // CI/GitOps endpoints - standard with user limits. 500 because `wayai push` makes four requests
5936
+ // (pull, diff, push, pull) and a GitOps job may push every hub at once with one token; when
5937
+ // each path held its own 100, that ran to 100 hubs a minute.
5938
+ "/api/ci/*": { tier: "STANDARD", userLimit: 500, skipAuthenticatedUsers: true, userLimitKey: "/api/ci/*" },
5939
+ // Attested `/v1` proxy onto the Rekor Worker. `userLimitKey` is load-bearing here: the
5940
+ // per-user budget defaults to the full request path, and this surface's paths carry base,
5941
+ // record, and file ids — so without a pinned key every distinct resource would get its own
5942
+ // fresh budget and the per-user limit would bound nothing. One shared budget across the
5943
+ // whole data surface is the intent.
5944
+ [`${DATA_PROXY_PREFIX}/*`]: {
5945
+ tier: "STANDARD",
5946
+ userLimit: 150,
5947
+ skipAuthenticatedUsers: true,
5948
+ userLimitKey: `${DATA_PROXY_PREFIX}/*`
5949
+ },
5950
+ // Voice calls — the end user's call routes (`routes/api/calls`). Every caller is signed
5951
+ // in, so the per-IP tier is skipped for them: Brazilian mobile carriers put many callers
5952
+ // behind one IP, and a shared bucket would refuse one caller for another's traffic.
5953
+ //
5954
+ // Create is the only route that spends: it opens a provider session on the org's own
5955
+ // key, billed per second. So it takes its own exact key and an hourly per-user cap, which
5956
+ // lives on Redis and is fail-closed like the other hourly caps. The exact key wins over
5957
+ // the wildcard below, which also matches `/api/calls` (see `getEndpointConfig`).
5958
+ "/api/calls": {
5959
+ tier: "STANDARD",
5960
+ userLimit: 30,
5961
+ userWindow: 3600,
5962
+ failClosed: true,
5963
+ skipAuthenticatedUsers: true,
5964
+ userLimitKey: "/api/calls"
5965
+ },
5966
+ // The routes on one call: `ready`, hang-up, the delegation report and the status read; and
5967
+ // the opening read (`/api/calls/openings/:language`), one per call, before it exists.
5968
+ // NEVER fail-closed: a refused hang-up leaves the call running, and billing, until a
5969
+ // limit ends it. The 60 s budget runs on the native binding, which fails open. The key is
5970
+ // pinned because the paths carry the call id, which would otherwise give every call a
5971
+ // fresh budget.
5972
+ "/api/calls/*": {
5973
+ tier: "STANDARD",
5974
+ userLimit: 150,
5975
+ skipAuthenticatedUsers: true,
5976
+ userLimitKey: "/api/calls/*"
5977
+ },
5978
+ // Contacts between accounts (gh #4661, `routes/api/contacts`). Every caller is signed in, so
5979
+ // the per-IP tier is skipped. The keys are pinned because the paths carry user and invite
5980
+ // ids, which would otherwise give each id a fresh budget. Blocks take their own, lower
5981
+ // budget (#4661 Limits: blocks are rate-limited). The invites-per-day cap and every list
5982
+ // cap are counted in the caller's UserDO, in the same write they bound, not here.
5983
+ "/api/contacts/*": {
5984
+ tier: "STANDARD",
5985
+ userLimit: 60,
5986
+ skipAuthenticatedUsers: true,
5987
+ userLimitKey: "/api/contacts/*"
5988
+ },
5989
+ "/api/contacts/blocks/*": {
5990
+ tier: "STANDARD",
5991
+ userLimit: 30,
5992
+ skipAuthenticatedUsers: true,
5993
+ userLimitKey: "/api/contacts/blocks/*"
5994
+ },
5995
+ // Person-to-person threads (gh #4661, `routes/api/threads`): the per-user send limit, 60 a
5996
+ // minute on the native binding. Every thread route shares the one budget, reads included:
5997
+ // the paths carry thread and message ids, so the key is pinned, and a pattern here cannot
5998
+ // tell a send from a read of the same path. Like every 60 s budget it fails open on a
5999
+ // binding error, so it bounds sends in the steady state only.
6000
+ "/api/threads/*": {
6001
+ tier: "STANDARD",
6002
+ userLimit: 60,
6003
+ skipAuthenticatedUsers: true,
6004
+ userLimitKey: "/api/threads/*"
6005
+ },
6006
+ // An org admin's erasure of one person's thread messages (gh #4661 owner decision 7): any user
6007
+ // can own a free org, so each request arms that org's alarm, and this caps how fast one person
6008
+ // can ask. The org's own daily cap (`PEOPLE_ORG_ERASURES_PER_DAY`, counted in its OrgDO in the
6009
+ // write it bounds) is the fail-closed bound; this budget, on the native binding, fails open.
6010
+ "/api/threads/org-erasures": {
6011
+ tier: "STANDARD",
6012
+ userLimit: 10,
6013
+ skipAuthenticatedUsers: true,
6014
+ userLimitKey: "/api/threads/org-erasures"
6015
+ },
6016
+ // A person's report of a message, thread or person (`people-reports.ts`): each one written
6017
+ // arms the PlatformDO's alarm and emails the platform admins, so it takes its own, lower
6018
+ // budget. The reporter's daily cap (`PEOPLE_REPORTS_PER_DAY`, counted in the report store in
6019
+ // the write it bounds) is the fail-closed bound; this budget, on the native binding, fails open.
6020
+ "/api/threads/reports": {
6021
+ tier: "STANDARD",
6022
+ userLimit: 10,
6023
+ skipAuthenticatedUsers: true,
6024
+ userLimitKey: "/api/threads/reports"
6025
+ },
6026
+ // Communities (gh #4872, `routes/api/communities`): every signed-in route under the mount (the
6027
+ // preview below excepted) shares one per-user budget, which is also each member's send limit and
6028
+ // bounds the reads that scan a community's rows (a title search, the stats); a community's own
6029
+ // limits (its rate, the daily caps, slow mode) are counted in its CommunityDO, in the write they
6030
+ // bound. Every caller is signed in, so the per-IP tier is skipped; the key is pinned because the
6031
+ // paths carry community, room and message ids.
6032
+ "/api/communities/*": {
6033
+ tier: "STANDARD",
6034
+ userLimit: 60,
6035
+ skipAuthenticatedUsers: true,
6036
+ userLimitKey: "/api/communities/*"
6037
+ },
6038
+ // The join page's preview of a link — public, fetched from the visitor's browser, so this
6039
+ // per-IP limit is its one bound and runs before the route reads anything (#4872
6040
+ // owner-delegated decision 9). MODERATE (30/min per IP) leaves room for reloads and several
6041
+ // tabs. An exact key, so it wins over the wildcard above.
6042
+ "/api/communities/join-preview": { tier: "MODERATE" },
6043
+ // An announcement email's unsubscribe (#4872 PR 8a) — public: its token is its one credential, so
6044
+ // this per-IP limit is its one bound, and runs before the route reads anything. MODERATE (30/min
6045
+ // per IP), not STRICT: mail clients' one-click unsubscribe POSTs it from their own servers, which
6046
+ // many members' clicks share, and STRICT's 5/min bucket is shared with sign-in. An exact key, so
6047
+ // it wins over the wildcard above.
6048
+ [COMMUNITY_EMAIL_UNSUBSCRIBE_PATH]: { tier: "MODERATE" },
6049
+ // The WayAI Guide's help entry (wayai#4948, `routes/api/guide.ts`): one POST per click by a viewer
6050
+ // the snapshot does not show enrolled, which reads the person's row on the Guide hub and writes only
6051
+ // what is missing (an enrolled viewer's click opens the hub without a request). Every caller is
6052
+ // signed in, so the per-IP tier is skipped; 10 a minute per person on the native binding (fails
6053
+ // open, like every 60 s budget) bounds what a looping client costs the one Guide HubDO.
6054
+ "/api/guide/access": {
6055
+ tier: "STANDARD",
6056
+ userLimit: 10,
6057
+ skipAuthenticatedUsers: true,
6058
+ userLimitKey: "/api/guide/access"
6059
+ },
6060
+ // App channel - high throughput with user limits
6061
+ "/channels/app/*": { tier: "HIGH", userLimit: 500, skipAuthenticatedUsers: true },
6062
+ // API channel (api-channel-connector) - machine-to-machine, high throughput
6063
+ "/channels/api/*": { tier: "HIGH", userLimit: 500, skipAuthenticatedUsers: true },
6064
+ // System/scheduled - high throughput (internal)
6065
+ "/channels/system/*": { tier: "HIGH", skipAuthenticatedUsers: true },
6066
+ "/scheduled/*": { tier: "HIGH", skipAuthenticatedUsers: true },
6067
+ // WebSocket - standard with user limit to prevent connection spam
6068
+ "/live-updates/ws": { tier: "STANDARD", userLimit: 100, failClosed: true },
6069
+ // A community's live socket (gh #4872 PR 5, `GET /communities/:communityId/live`): outside
6070
+ // `/api/*`, with no bearer token, so its per-IP tier is its one bound before the CommunityDO
6071
+ // checks the ticket. STANDARD, as `/live-updates/ws`: a client opens one per community it shows
6072
+ // and reconnects within the hour.
6073
+ "/communities/*": { tier: "STANDARD" },
6074
+ // WS ticket mint - per-user 60/min accommodates aggressive reconnect patterns
6075
+ // without opening abuse vectors; fail-closed so Redis outages don't let
6076
+ // runaway clients mint unbounded tickets.
6077
+ "/api/auth/ws-ticket": { tier: "MODERATE", userLimit: 60, failClosed: true, skipAuthenticatedUsers: true },
6078
+ "/api/auth/session-check": { tier: "STANDARD", userLimit: 120, skipAuthenticatedUsers: true },
6079
+ "/api/auth/logout": { tier: "MODERATE", userLimit: 10, failClosed: true, skipAuthenticatedUsers: true }
6080
+ };
6081
+ EXCLUDED_PATHS = /* @__PURE__ */ new Set(["/health"]);
6082
+ }
6083
+ });
6084
+
6085
+ // ../../packages/core/dist/chunk-CGOLULK2.js
6086
+ function fileSnapshotChanged(run) {
6087
+ return run.written + run.removed > 0;
6088
+ }
6089
+ async function runFileSnapshot(opts) {
6090
+ const run = { pages: 0, written: 0, removed: 0, unchanged: 0 };
6091
+ const seen = /* @__PURE__ */ new Set();
6092
+ let cursor;
6093
+ while (run.pages < FILE_SNAPSHOT_MAX_PAGES) {
6094
+ const page = await sendWithRetry(opts, cursor);
6095
+ run.pages++;
6096
+ run.written += page.written;
6097
+ run.removed += page.removed;
6098
+ run.unchanged += page.unchanged;
6099
+ opts.onPage?.(page, run.pages);
6100
+ if (page.next_cursor === null) return run;
6101
+ if (seen.has(page.next_cursor)) {
6102
+ throw new Error(`The preview file snapshot repeated its cursor after page ${run.pages}, so it would never end.`);
6103
+ }
6104
+ seen.add(page.next_cursor);
6105
+ cursor = page.next_cursor;
6106
+ }
6107
+ throw new Error(`The preview file snapshot did not end after ${FILE_SNAPSHOT_MAX_PAGES} pages.`);
6108
+ }
6109
+ async function sendWithRetry(opts, cursor) {
6110
+ for (let attempt = 1; ; attempt++) {
6111
+ let body;
6112
+ try {
6113
+ body = await opts.sendPage(cursor);
6114
+ } catch (error) {
6115
+ if (attempt < FILE_SNAPSHOT_CONFLICT_ATTEMPTS && opts.isConflict(error)) continue;
6116
+ throw error;
6117
+ }
6118
+ const parsed = rekorFileSnapshotAnswerSchema.safeParse(body);
6119
+ if (!parsed.success) {
6120
+ throw new Error("The data product answered a preview file snapshot page WayAI cannot read: nothing more was sent.");
6121
+ }
6122
+ return parsed.data.data;
6123
+ }
6124
+ }
6125
+ var rekorFileSnapshotAnswerSchema, FILE_SNAPSHOT_MAX_PAGES, FILE_SNAPSHOT_CONFLICT_ATTEMPTS, FILE_SNAPSHOT_PAGE_SPACING_MS, FILE_SNAPSHOT_CONFLICT_STATUS;
6126
+ var init_chunk_CGOLULK2 = __esm({
6127
+ "../../packages/core/dist/chunk-CGOLULK2.js"() {
6128
+ "use strict";
6129
+ init_chunk_IEWJ5MZH();
6130
+ init_chunk_F3JHI4Y4();
6131
+ init_zod();
6132
+ rekorFileSnapshotAnswerSchema = external_exports.object({
6133
+ data: external_exports.object({
6134
+ next_cursor: external_exports.string().nullable(),
6135
+ /** Files the page created or replaced on the preview. */
6136
+ written: external_exports.number().int(),
6137
+ /** The preview's own files, ones production does not hold, the page removed. */
6138
+ removed: external_exports.number().int(),
6139
+ /** Files the page found already equal to production's. */
6140
+ unchanged: external_exports.number().int()
6141
+ })
6142
+ });
6143
+ FILE_SNAPSHOT_MAX_PAGES = 1e4;
6144
+ FILE_SNAPSHOT_CONFLICT_ATTEMPTS = 3;
6145
+ FILE_SNAPSHOT_PAGE_SPACING_MS = Math.ceil(
6146
+ 6e4 / Math.max(1, Math.floor((getEndpointConfig(`${DATA_PROXY_PREFIX}/files/snapshot`)?.userLimit ?? 60) * 4 / 5))
6147
+ );
6148
+ FILE_SNAPSHOT_CONFLICT_STATUS = 409;
6149
+ }
6150
+ });
6151
+
5556
6152
  // ../../packages/core/dist/chunk-BEOHBREG.js
5557
6153
  var REKOR_ACTOR_TYPE_HEADER, REKOR_ACTOR_ID_HEADER, REKOR_ACTOR_LABEL_HEADER, REKOR_ACTOR_ORG_HEADER, REKOR_ACTOR_ADMIN_HEADER, REKOR_CLIENT_HEADER, REKOR_TOOL_ID_HEADER, REKOR_CLIENTS, rekorClientSchema, MAX_REKOR_TOOL_ID_LENGTH, rekorToolIdSchema, REKOR_V1_PREFIX, DATA_PROXY_ORG_QUERY_PARAM, MAX_PROVISIONING_BODY_BYTES, dataProxyQuery, rekorAttestation, REQUIRED_REKOR_ATTESTATION_HEADERS, REKOR_BOUND_BASE_HEADER, REKOR_BOUND_TOOLSET_HEADER, REKOR_BOUND_TOKEN_HEADER, REKOR_BOUND_CUSTOMER_HEADER, DATA_BASE_GENERATION_HEADER, REKOR_MAX_ACTOR_ID_LENGTH, REKOR_MAX_ACTOR_LABEL_LENGTH;
5558
6154
  var init_chunk_BEOHBREG = __esm({
@@ -5596,10 +6192,10 @@ var init_chunk_BEOHBREG = __esm({
5596
6192
  }
5597
6193
  });
5598
6194
 
5599
- // ../../packages/core/dist/chunk-BVQE66I2.js
6195
+ // ../../packages/core/dist/chunk-4MRBBR5L.js
5600
6196
  var TOOLSET_SLUG_PATTERN, REKOR_STRUCTURAL_ID_PATTERN, baseEnvironmentSchema, TOOLSET_FILE_OPERATIONS, toolsetFileToolSchema, toolsetSurfaceSchema, toolsetConnectionBindingSchema, rekorToolsetToolsSchema, rekorBoundAttestation, rekorResolvedToolsetSchema, resolvedFileToolSchema, rekorToolsetFileSchema, rekorToolsetFileListSchema, rekorToolsetFileGetSchema, rekorToolsetFilePutSchema, TOOLSET_CUSTOMER_FACING_REFUSAL_MARK, TOOLSET_CUSTOMER_FACING_REFUSAL;
5601
- var init_chunk_BVQE66I2 = __esm({
5602
- "../../packages/core/dist/chunk-BVQE66I2.js"() {
6197
+ var init_chunk_4MRBBR5L = __esm({
6198
+ "../../packages/core/dist/chunk-4MRBBR5L.js"() {
5603
6199
  "use strict";
5604
6200
  init_chunk_BEOHBREG();
5605
6201
  init_chunk_DHPUPI4Y();
@@ -5900,25 +6496,6 @@ var init_chunk_IPDOHAZ5 = __esm({
5900
6496
  }
5901
6497
  });
5902
6498
 
5903
- // ../../packages/core/dist/chunk-GPM7PE44.js
5904
- var bootstrapQuery;
5905
- var init_chunk_GPM7PE44 = __esm({
5906
- "../../packages/core/dist/chunk-GPM7PE44.js"() {
5907
- "use strict";
5908
- init_zod();
5909
- bootstrapQuery = external_exports.object({
5910
- active_hub_id: external_exports.string().uuid().optional(),
5911
- active_conversation_id: external_exports.string().uuid().optional(),
5912
- active_nav: external_exports.enum(["chat", "task", "support"]).optional(),
5913
- /**
5914
- * The people thread the URL names (`/chat/threads/:threadId`, gh #4661): the snapshot
5915
- * preloads it as `people.active_thread` when the viewer is a member.
5916
- */
5917
- active_thread_id: external_exports.string().uuid().optional()
5918
- });
5919
- }
5920
- });
5921
-
5922
6499
  // ../../packages/core/dist/chunk-YUZFHVB2.js
5923
6500
  var invitePreviewQuery, inviteRolePreviewResponse, inviteContactPreviewResponse, invitePreviewResponse, inviteUnsubscribeBody;
5924
6501
  var init_chunk_YUZFHVB2 = __esm({
@@ -7544,15 +8121,15 @@ var init_chunk_MH4O3YEX = __esm({
7544
8121
  }
7545
8122
  });
7546
8123
 
7547
- // ../../packages/core/dist/chunk-LDSABSRL.js
8124
+ // ../../packages/core/dist/chunk-HX2LNKHX.js
7548
8125
  function catalogToolSideEffects(toolName) {
7549
8126
  const sideEffects = CATALOG_SIDE_EFFECTS.get(toolName);
7550
8127
  if (sideEffects === void 0) return "unknown";
7551
8128
  return sideEffects ? "side_effects" : "none";
7552
8129
  }
7553
8130
  var NATIVE_TOOL_SCHEMAS, WAYAI_CONNECTOR, SANDBOX_AGENT_ROLES, BASE_NATIVE_TOOLS, NATIVE_TOOLS, CATALOG_SIDE_EFFECTS;
7554
- var init_chunk_LDSABSRL = __esm({
7555
- "../../packages/core/dist/chunk-LDSABSRL.js"() {
8131
+ var init_chunk_HX2LNKHX = __esm({
8132
+ "../../packages/core/dist/chunk-HX2LNKHX.js"() {
7556
8133
  "use strict";
7557
8134
  init_chunk_DHPUPI4Y();
7558
8135
  NATIVE_TOOL_SCHEMAS = {
@@ -7900,14 +8477,14 @@ var init_chunk_LDSABSRL = __esm({
7900
8477
  "copy_to_sandbox": {
7901
8478
  "tool_config": {
7902
8479
  "name": "copy_to_sandbox",
7903
- "description": `Copy one of this conversation's files into the code sandbox, so commands run with bash can read or edit it there. path names the file as read_file does: this conversation's attachments under "conversation/" (e.g. "conversation/report.xlsx"), or an earlier conversation's under "conversations/<conversation_id>/<name>". Hub resource files cannot be copied. The copy lands at sandbox_path, replacing any file there; a file larger than 25 MiB is refused. If no sandbox is running, the copy starts an empty one.`,
8480
+ "description": `Copy one of this conversation's files, or one of your documents, into the code sandbox, so commands run with bash can read or edit it there. path names the file as read_file does: this conversation's attachments under "conversation/" (e.g. "conversation/report.xlsx"), an earlier conversation's under "conversations/<conversation_id>/<name>", or a document under "data/<file-type>/" (e.g. "data/contracts/acme/msa.docx"), whose result gives the version you copied: pass it to copy_from_sandbox to copy the document back. Hub resource files cannot be copied. The copy lands at sandbox_path, replacing any file there; a file larger than 25 MiB is refused, and a document larger than 1 MiB. If no sandbox is running, the copy starts an empty one.`,
7904
8481
  "strict": false,
7905
8482
  "parameters": {
7906
8483
  "type": "object",
7907
8484
  "properties": {
7908
8485
  "path": {
7909
8486
  "type": "string",
7910
- "description": `The file to copy, as read_file names it: "conversation/<name>" (a shorter unambiguous form or the file's id also works) or "conversations/<conversation_id>/<name>".`
8487
+ "description": `The file to copy, as read_file names it: "conversation/<name>" (a shorter unambiguous form or the file's id also works), "conversations/<conversation_id>/<name>", or a document's full path "data/<file-type>/<path>".`
7911
8488
  },
7912
8489
  "sandbox_path": {
7913
8490
  "type": "string",
@@ -7925,7 +8502,7 @@ var init_chunk_LDSABSRL = __esm({
7925
8502
  "copy_from_sandbox": {
7926
8503
  "tool_config": {
7927
8504
  "name": "copy_from_sandbox",
7928
- "description": 'Copy a file out of the code sandbox into this conversation, so what your commands produced (a chart, a converted document) is kept after the sandbox ends. The file is stored for the support team on an internal note and is NOT sent to the customer: the result gives its path under "conversation/", and send_files with that path delivers it when it should reach them. A file larger than 25 MiB is refused.',
8505
+ "description": `Copy a file out of the code sandbox into this conversation, so what your commands produced (a chart, a converted document) is kept after the sandbox ends. The file is stored for the support team on an internal note and is NOT sent to the customer: the result gives its path under "conversation/", and send_files with that path delivers it when it should reach them. A file larger than 25 MiB is refused. With path, the file is written instead to one of your documents under "data/<file-type>/", with the version you copied it in at: the write goes through only while that is still the document's current version, so a document changed since is refused, and only where you may write documents. A document holds at most 1 MiB.`,
7929
8506
  "strict": false,
7930
8507
  "parameters": {
7931
8508
  "type": "object",
@@ -7936,7 +8513,15 @@ var init_chunk_LDSABSRL = __esm({
7936
8513
  },
7937
8514
  "name": {
7938
8515
  "type": "string",
7939
- "description": `The stored file's name, with no folders (e.g. "q3-report.xlsx"). Defaults to the file's name in the sandbox.`
8516
+ "description": `The stored file's name, with no folders (e.g. "q3-report.xlsx"). Defaults to the file's name in the sandbox. Not with path.`
8517
+ },
8518
+ "path": {
8519
+ "type": "string",
8520
+ "description": 'A document to write the file to instead, by its full path, e.g. "data/contracts/acme/msa.docx", as copy_to_sandbox or list_files gives it. Leave it out to keep the file with this conversation.'
8521
+ },
8522
+ "version": {
8523
+ "type": "integer",
8524
+ "description": "With path: the version copy_to_sandbox (or read_file) gave for the document, to replace it. Leave it out to create a new document."
7940
8525
  }
7941
8526
  },
7942
8527
  "required": [
@@ -8524,11 +9109,17 @@ var init_chunk_LDSABSRL = __esm({
8524
9109
  // setting is on, on any provider. Side effects: under an Allowlist or Web network setting, the
8525
9110
  // code it runs can send data out of the conversation.
8526
9111
  { tool_native_id: "nt-030", tool_name: "bash", tool_display_name: "Run Code (Bash)", tool_description: "Run a Bash command in the conversation's code sandbox", connector_id: WAYAI_CONNECTOR, operation: "bash", tool_method: "internal", tool_group: "Native Tools", is_delegation: false, is_auto_set: false, execution_config: null, side_effects: true, replay: "reconcile", assignable_roles: SANDBOX_AGENT_ROLES },
8527
- // Copies a conversation file INTO the sandbox: the sandbox is conversation state, so nothing
8528
- // leaves the conversation.
9112
+ // Copies a conversation file, or a toolset document (`data/`, read through the toolset's `get`),
9113
+ // INTO the sandbox: the sandbox is conversation state, so nothing leaves the conversation. It is
9114
+ // shared by the conversation's agents that use it, so a document copied in is there for each of
9115
+ // them, as anything else an agent leaves in it.
8529
9116
  { tool_native_id: "nt-031", tool_name: "copy_to_sandbox", tool_display_name: "Copy File to Sandbox", tool_description: "Copy one of the conversation's files into its code sandbox", connector_id: WAYAI_CONNECTOR, operation: "copy_to_sandbox", tool_method: "internal", tool_group: "Native Tools", is_delegation: false, is_auto_set: false, execution_config: null, side_effects: false, replay: "rerun", assignable_roles: SANDBOX_AGENT_ROLES },
8530
9117
  // Copies a sandbox file OUT into the conversation, as an internal note for the team: nothing
8531
- // reaches the customer until `send_files` sends it.
9118
+ // reaches the customer until `send_files` sends it. With `path`, it writes a toolset document
9119
+ // instead, through the toolset's `put` with its version check: such a call is `reconcile`, not
9120
+ // `keyed`, as `write_file` is (`toolCallReplay` reads its arguments). Its side effects stay `false`:
9121
+ // only an agent holding a toolset `put` can write one, never a customer-facing role (premise 8),
9122
+ // and a voice call's turns, which read this flag, run the pilot track, which is customer-facing.
8532
9123
  { tool_native_id: "nt-032", tool_name: "copy_from_sandbox", tool_display_name: "Copy File from Sandbox", tool_description: "Copy a file out of the code sandbox into the conversation, for the team", connector_id: WAYAI_CONNECTOR, operation: "copy_from_sandbox", tool_method: "internal", tool_group: "Native Tools", is_delegation: false, is_auto_set: false, execution_config: null, side_effects: false, replay: "keyed", assignable_roles: SANDBOX_AGENT_ROLES },
8533
9124
  // The `data/` mount's writes (#5142 step 2): a document of the agent's Base Toolset, written
8534
9125
  // through the toolset's own `put` tool with its version check. `reconcile`, not `keyed`: a re-run
@@ -8764,7 +9355,7 @@ var init_chunk_T7KQKDET = __esm({
8764
9355
  }
8765
9356
  });
8766
9357
 
8767
- // ../../packages/core/dist/chunk-7EGDXEYG.js
9358
+ // ../../packages/core/dist/chunk-BUXNYLB7.js
8768
9359
  function resolveMonitorTrigger(monitorConfig) {
8769
9360
  if (!monitorConfig || typeof monitorConfig !== "object") return "idle";
8770
9361
  const raw = monitorConfig.trigger;
@@ -9070,10 +9661,10 @@ function refineMonitorRulesStructuredOutput(body, ctx) {
9070
9661
  if (refusal2) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["response_format"], message: refusal2 });
9071
9662
  }
9072
9663
  var NATIVE_TOOL_NAMES, previousConversationsCountField, summarizationThresholdField, flagConditionSchema, MONITOR_TRIGGERS, monitorTriggerSchema, AUTOMATION_GATE_TRIGGERS, NON_FIRING_TRIGGERS, MONITOR_FIRING_TRIGGERS, MONITOR_DELAY_SECONDS_MIN, MONITOR_IDLE_DELAY_REQUIRED_MESSAGE, MONITOR_HISTORY_MESSAGES_MAX, monitorHistoryMessagesSchema, monitorIncludeToolResultsSchema, MONITOR_INPUT_SHAPING_KEYS, MONITOR_INPUT_SHAPING_SCHEMAS, MONITOR_INPUT_SHAPING_IDLE_MESSAGE, monitorArgumentSourceSchema, MONITOR_NOTE_TEMPLATE_MAX, MONITOR_STEER_NOTE_MAX, MONITOR_GATE_AGENT_NAME_MAX, monitorActionSchema, monitorRuleSchema, MONITOR_RULE_KEYS, MONITOR_RULE_SCHEMAS, MONITOR_RULE_TRIGGERS, MONITOR_RULE_TRIGGER_MESSAGE, CALL_UTTERANCE_SPEAKERS, callUtteranceSpeakerSchema, MONITOR_CALL_KEYS, MONITOR_CALL_SCHEMAS, MONITOR_CALL_KEY_MESSAGE, CALL_STEERING_MIN_CONFIDENCE, CALL_UTTERANCE_RULE_NOT_CONFIDENT_MESSAGE, CALL_UTTERANCE_FALLBACK_MESSAGE, CALL_UTTERANCE_FLAG_CONDITIONS_MESSAGE, CALL_STEER_NOTE_BLANK_MESSAGE, MONITOR_USER_MESSAGE_ACTION_KINDS, MONITOR_ASSISTANT_REPLY_ACTION_KINDS, MONITOR_CALL_UTTERANCE_ACTION_KINDS, MONITOR_AUTOMATION_FIRE_ACTION_KINDS, MONITOR_AUTOMATION_ITEM_ACTION_KINDS, INSERT_NOTE_TOOL_NAME, RUN_MONITOR_TOOL_NAME, MONITOR_RULE_ALLOWED_NATIVE_TOOLS, MONITOR_RULE_REENTRY_TOOLS, MONITOR_RULE_REENTRY_TRACKS, AUTOMATION_FIRE_FLAG_CONDITIONS_MESSAGE, AUTOMATION_FIRE_INPUT_SHAPING_MESSAGE, AUTOMATION_ITEM_FLAG_CONDITIONS_MESSAGE, AUTOMATION_ITEM_INPUT_SHAPING_MESSAGE, AUTOMATION_GATE_KEY_MESSAGES, monitorConfigField, flagConditionsField, agentIdParam, listAgentsQuery, agentConnectionsQuery, createAgentBody, updateAgentBody, AGENT_PARAMETER_TYPES, AGENT_PARAMETER_NAME_REGEX, agentParameterCreateSchema, MAX_AGENT_PARAMETERS_PER_REQUEST, createAgentParametersBody;
9073
- var init_chunk_7EGDXEYG = __esm({
9074
- "../../packages/core/dist/chunk-7EGDXEYG.js"() {
9664
+ var init_chunk_BUXNYLB7 = __esm({
9665
+ "../../packages/core/dist/chunk-BUXNYLB7.js"() {
9075
9666
  "use strict";
9076
- init_chunk_LDSABSRL();
9667
+ init_chunk_HX2LNKHX();
9077
9668
  init_chunk_IBL7FANV();
9078
9669
  init_chunk_PFDZEKDE();
9079
9670
  init_chunk_HZ6CADSY();
@@ -9296,12 +9887,12 @@ var init_chunk_7EGDXEYG = __esm({
9296
9887
  }
9297
9888
  });
9298
9889
 
9299
- // ../../packages/core/dist/chunk-3OWAWA5B.js
9890
+ // ../../packages/core/dist/chunk-7YJWGPAR.js
9300
9891
  var conversationIdParam, conversationHubQuery, claimBody, releaseBody, transferBody, closeBody, additionalContextPayloadSchema, kanbanUpdateBody, annotateConversationBody, observabilityParams, observabilityListParams, deleteUserConversationsBody, deleteUserConversationsResponse, sendMessageBody, listConversationsQuery, flagSourceSchema, getConversationsResponse, getConversationDetailParams, getConversationDetailQuery, getConversationDetailResponse, listWhatsAppTemplatesQuery, sendWhatsAppTemplateBody;
9301
- var init_chunk_3OWAWA5B = __esm({
9302
- "../../packages/core/dist/chunk-3OWAWA5B.js"() {
9892
+ var init_chunk_7YJWGPAR = __esm({
9893
+ "../../packages/core/dist/chunk-7YJWGPAR.js"() {
9303
9894
  "use strict";
9304
- init_chunk_7EGDXEYG();
9895
+ init_chunk_BUXNYLB7();
9305
9896
  init_chunk_HZ6CADSY();
9306
9897
  init_chunk_DHPUPI4Y();
9307
9898
  init_zod();
@@ -9757,7 +10348,7 @@ var init_chunk_BPCNB5RT = __esm({
9757
10348
  }
9758
10349
  });
9759
10350
 
9760
- // ../../packages/core/dist/chunk-ROS7PLDY.js
10351
+ // ../../packages/core/dist/chunk-AR73HGPH.js
9761
10352
  function idempotencyHeaderRefusal(value) {
9762
10353
  if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/.test(value)) {
9763
10354
  return "idempotency_header must be a header name: letters, digits and dashes, at most 64";
@@ -9768,10 +10359,10 @@ function idempotencyHeaderRefusal(value) {
9768
10359
  return null;
9769
10360
  }
9770
10361
  var CATALOG_REPLAY, REPLAY_OVERRIDES, RESERVED_HEADERS;
9771
- var init_chunk_ROS7PLDY = __esm({
9772
- "../../packages/core/dist/chunk-ROS7PLDY.js"() {
10362
+ var init_chunk_AR73HGPH = __esm({
10363
+ "../../packages/core/dist/chunk-AR73HGPH.js"() {
9773
10364
  "use strict";
9774
- init_chunk_LDSABSRL();
10365
+ init_chunk_HX2LNKHX();
9775
10366
  CATALOG_REPLAY = new Map(
9776
10367
  NATIVE_TOOLS.map((tool) => [tool.tool_name, tool.replay])
9777
10368
  );
@@ -10004,7 +10595,7 @@ var init_chunk_WEXVYJ5H = __esm({
10004
10595
  }
10005
10596
  });
10006
10597
 
10007
- // ../../packages/core/dist/chunk-AFIQ3FEW.js
10598
+ // ../../packages/core/dist/chunk-MK6D4I2Q.js
10008
10599
  function refineToolConfig(value, ctx) {
10009
10600
  const raw = value.tool_config;
10010
10601
  if (raw == null) return;
@@ -10040,10 +10631,10 @@ function refineToolConfig(value, ctx) {
10040
10631
  }
10041
10632
  }
10042
10633
  var toolIdParam, listToolsQuery, toolAgentQuery, deleteToolQuery, toolConnectionsQuery, nativeToolIdSchema, CONTEXT_BOUNDARIES, contextBoundarySchema, addNativeToolBody, addMcpToolBody, toolReplaySettingsFields, toolReplaySettingsSchema, createCustomToolBody, updateCustomToolBody, updateToolExecutionConfigBody;
10043
- var init_chunk_AFIQ3FEW = __esm({
10044
- "../../packages/core/dist/chunk-AFIQ3FEW.js"() {
10634
+ var init_chunk_MK6D4I2Q = __esm({
10635
+ "../../packages/core/dist/chunk-MK6D4I2Q.js"() {
10045
10636
  "use strict";
10046
- init_chunk_ROS7PLDY();
10637
+ init_chunk_AR73HGPH();
10047
10638
  init_chunk_WEXVYJ5H();
10048
10639
  init_chunk_HZ6CADSY();
10049
10640
  init_zod();
@@ -11415,7 +12006,7 @@ var init_chunk_6NI5RAM4 = __esm({
11415
12006
  }
11416
12007
  });
11417
12008
 
11418
- // ../../packages/core/dist/chunk-NYIIBG23.js
12009
+ // ../../packages/core/dist/chunk-EAT2SP4E.js
11419
12010
  function refineHubAsCodeCustomTools(config, ctx) {
11420
12011
  const agents = config.agents;
11421
12012
  if (!Array.isArray(agents)) return;
@@ -11951,12 +12542,12 @@ function refineHubAsCodeMonitorRulesOutput(config, ctx) {
11951
12542
  });
11952
12543
  }
11953
12544
  var MAX_10MB_BASE64_ENCODED_BYTES, MAX_EVAL_ATTACHMENT_ENCODED_BYTES, MAX_EVAL_ATTACHMENT_CARRIERS, MAX_EVAL_ATTACHMENT_AGGREGATE_ENCODED_BYTES, MAX_RESOURCE_ENCODED_BYTES, MAX_HUB_AS_CODE_RESOURCES, MAX_HUB_AS_CODE_RESOURCE_FILES, MAX_RESOURCE_AGGREGATE_ENCODED_BYTES, MAX_CI_CONFIG_BODY_BYTES, MAX_CALL_OPENING_CARRIERS, MAX_CALL_OPENING_AGGREGATE_ENCODED_BYTES, hubAsCodeStateSchema, hubAsCodeResourceFileSchema, hubAsCodeResourceSchema, uuidPattern, ciUuidSchema, boundedHubAsCodeResourcesSchema, hubAsCodeConfigSchema, ciPullHubIdParam, ciBranchParam, ciPullQuery, ciHubsQuery, ciLookupQuery, ciBranchesQuery, ciSyncBody, ciPushBody, ciDiffBody, ciPublishBody, ciPublishPreviewBody, ciSyncMcpBody, ciOrgResourcesParam, orgResourcesConfigSchema, ciOrgResourcesPushBody, ciOrgResourcesDiffBody, CI_APPLY_ENTITY_KINDS, CI_APPLY_OPERATIONS, ciApplyErrorBaseSchema, ciApplyErrorSchema, ciSyncCountSchema, ciSyncChangesSchema, ciSyncWarningSchema, ciSyncResponseSchema;
11954
- var init_chunk_NYIIBG23 = __esm({
11955
- "../../packages/core/dist/chunk-NYIIBG23.js"() {
12545
+ var init_chunk_EAT2SP4E = __esm({
12546
+ "../../packages/core/dist/chunk-EAT2SP4E.js"() {
11956
12547
  "use strict";
11957
- init_chunk_AFIQ3FEW();
12548
+ init_chunk_MK6D4I2Q();
11958
12549
  init_chunk_623SRYWZ();
11959
- init_chunk_7EGDXEYG();
12550
+ init_chunk_BUXNYLB7();
11960
12551
  init_chunk_LBDH7LCG();
11961
12552
  init_chunk_4LXDRTO4();
11962
12553
  init_chunk_T4Q5COKJ();
@@ -12965,29 +13556,13 @@ var init_chunk_LQY2OCQL = __esm({
12965
13556
  }
12966
13557
  });
12967
13558
 
12968
- // ../../packages/core/dist/chunk-F3JHI4Y4.js
12969
- var DATA_PROXY_MOUNT, DATA_PROXY_PREFIX, BASE_DESTRUCTION_MOUNT, BASE_DESTRUCTION_PREFIX, BASE_DESTRUCTION_CONFIRM_PATH, COMMUNITY_EMAIL_UNSUBSCRIBE_PATH, CONTACT_BOOK_EMAIL_UNSUBSCRIBE_PATH, AUTOMATION_WEBHOOK_MOUNT;
12970
- var init_chunk_F3JHI4Y4 = __esm({
12971
- "../../packages/core/dist/chunk-F3JHI4Y4.js"() {
12972
- "use strict";
12973
- DATA_PROXY_MOUNT = "/data";
12974
- DATA_PROXY_PREFIX = `/api${DATA_PROXY_MOUNT}`;
12975
- BASE_DESTRUCTION_MOUNT = "/bases";
12976
- BASE_DESTRUCTION_PREFIX = `/api${BASE_DESTRUCTION_MOUNT}`;
12977
- BASE_DESTRUCTION_CONFIRM_PATH = `${BASE_DESTRUCTION_PREFIX}/destruction/confirm`;
12978
- COMMUNITY_EMAIL_UNSUBSCRIBE_PATH = "/api/communities/email-unsubscribe";
12979
- CONTACT_BOOK_EMAIL_UNSUBSCRIBE_PATH = "/api/contact-book/email-unsubscribe";
12980
- AUTOMATION_WEBHOOK_MOUNT = "/webhooks/automations";
12981
- }
12982
- });
12983
-
12984
- // ../../packages/core/dist/chunk-GCKAPAPC.js
13559
+ // ../../packages/core/dist/chunk-6N2QIHHQ.js
12985
13560
  var AUTOMATION_TRIGGER_TYPES, AUTOMATION_EVENT_TYPES, AUTOMATION_TARGET_TYPES, AUTOMATION_ACTION_TYPES, AUTOMATION_CHANNEL_TYPES, AUTOMATION_PURPOSES, AUTOMATION_NAME_MAX, AUTOMATION_DESCRIPTION_MAX, AUTOMATION_INSTRUCTIONS_MAX, AUTOMATION_TEXT_MAX, AUTOMATION_CRON_MAX, AUTOMATION_WEBHOOK_AUTHS, AUTOMATION_WEBHOOK_HEADERS, AUTOMATION_WEBHOOK_BODY_MAX_BYTES, AUTOMATION_WEBHOOK_HEADER_NAME_MAX, WEBHOOK_RESERVED_HEADERS, automationTriggerSchema, automationTargetSchema, automationActionSchema, automationGateSchema, automationName, automationDescription, automationIdParam, automationHubQuery, automationWebhookParams, AUTOMATION_RUNS_PAGE_MAX, listAutomationRunsQuery, createAutomationBody, updateAutomationBody, AUTOMATION_ALERT_CATEGORIES, AUTOMATION_BLOCKED_ALERT_CATEGORY, automationEventSubscriberParam;
12986
- var init_chunk_GCKAPAPC = __esm({
12987
- "../../packages/core/dist/chunk-GCKAPAPC.js"() {
13561
+ var init_chunk_6N2QIHHQ = __esm({
13562
+ "../../packages/core/dist/chunk-6N2QIHHQ.js"() {
12988
13563
  "use strict";
12989
13564
  init_chunk_LQY2OCQL();
12990
- init_chunk_7EGDXEYG();
13565
+ init_chunk_BUXNYLB7();
12991
13566
  init_chunk_3HSB6J4J();
12992
13567
  init_chunk_F3JHI4Y4();
12993
13568
  init_chunk_DHPUPI4Y();
@@ -14232,6 +14807,7 @@ var init_chunk_TS4EYU3W = __esm({
14232
14807
  var init_contracts = __esm({
14233
14808
  "../../packages/core/dist/contracts/index.js"() {
14234
14809
  "use strict";
14810
+ init_chunk_VXEAR36B();
14235
14811
  init_chunk_TIZ57ZBA();
14236
14812
  init_chunk_Q7KIQHAZ();
14237
14813
  init_chunk_2U4BTOR7();
@@ -14239,16 +14815,16 @@ var init_contracts = __esm({
14239
14815
  init_chunk_GK27ENTT();
14240
14816
  init_chunk_SXDX4GHS();
14241
14817
  init_chunk_FK6HBXM7();
14242
- init_chunk_VXEAR36B();
14818
+ init_chunk_GPM7PE44();
14243
14819
  init_chunk_DGKMMFIR();
14244
14820
  init_chunk_ZC6YGHNX();
14245
14821
  init_chunk_ACKF2MVU();
14246
14822
  init_chunk_Y7CFCYX4();
14247
14823
  init_chunk_5DGHIY7L();
14248
- init_chunk_BVQE66I2();
14824
+ init_chunk_CGOLULK2();
14825
+ init_chunk_4MRBBR5L();
14249
14826
  init_chunk_BEOHBREG();
14250
14827
  init_chunk_IPDOHAZ5();
14251
- init_chunk_GPM7PE44();
14252
14828
  init_chunk_YUZFHVB2();
14253
14829
  init_chunk_34LE6P7E();
14254
14830
  init_chunk_FPSDNS2E();
@@ -14261,12 +14837,12 @@ var init_contracts = __esm({
14261
14837
  init_chunk_LWMLR2WI();
14262
14838
  init_chunk_ACGAI25T();
14263
14839
  init_chunk_MH4O3YEX();
14264
- init_chunk_3OWAWA5B();
14840
+ init_chunk_7YJWGPAR();
14265
14841
  init_chunk_KSTYTQ7I();
14266
14842
  init_chunk_LKBSHANA();
14267
14843
  init_chunk_BPCNB5RT();
14268
- init_chunk_NYIIBG23();
14269
- init_chunk_AFIQ3FEW();
14844
+ init_chunk_EAT2SP4E();
14845
+ init_chunk_MK6D4I2Q();
14270
14846
  init_chunk_623SRYWZ();
14271
14847
  init_chunk_JDJ4EUOL();
14272
14848
  init_chunk_3KYTITOR();
@@ -14276,9 +14852,9 @@ var init_contracts = __esm({
14276
14852
  init_chunk_YULHXJ5M();
14277
14853
  init_chunk_V4FRPRH5();
14278
14854
  init_chunk_ZHJEQBXJ();
14279
- init_chunk_GCKAPAPC();
14855
+ init_chunk_6N2QIHHQ();
14280
14856
  init_chunk_LQY2OCQL();
14281
- init_chunk_7EGDXEYG();
14857
+ init_chunk_BUXNYLB7();
14282
14858
  init_chunk_3HSB6J4J();
14283
14859
  init_chunk_IG6QBZMA();
14284
14860
  init_chunk_ZJJ5ODOD();
@@ -14288,8 +14864,8 @@ var init_contracts = __esm({
14288
14864
  init_chunk_E4DIIOOE();
14289
14865
  init_chunk_7CYHPF35();
14290
14866
  init_chunk_FMXFEZN4();
14291
- init_chunk_ROS7PLDY();
14292
- init_chunk_LDSABSRL();
14867
+ init_chunk_AR73HGPH();
14868
+ init_chunk_HX2LNKHX();
14293
14869
  init_chunk_NVKU5PPQ();
14294
14870
  init_chunk_2FCLSQSQ();
14295
14871
  init_chunk_FECLIZTY();
@@ -14302,6 +14878,7 @@ var init_contracts = __esm({
14302
14878
  init_chunk_OMPT4ZHR();
14303
14879
  init_chunk_2PZIF56U();
14304
14880
  init_chunk_UEJOG2Q7();
14881
+ init_chunk_IEWJ5MZH();
14305
14882
  init_chunk_SOJMWM63();
14306
14883
  init_chunk_WEXVYJ5H();
14307
14884
  init_chunk_LZDVDC2S();
@@ -15523,32 +16100,6 @@ var init_chunk_BF4WRSOQ = __esm({
15523
16100
  });
15524
16101
 
15525
16102
  // ../../packages/core/dist/index.js
15526
- function matchPath(pattern, path42) {
15527
- const normalizedPath = path42.replace(/\/$/, "");
15528
- if (pattern.endsWith("/*")) {
15529
- const prefix = pattern.slice(0, -2);
15530
- return normalizedPath === prefix || normalizedPath.startsWith(prefix + "/");
15531
- }
15532
- return pattern === normalizedPath;
15533
- }
15534
- function getEndpointConfig(path42) {
15535
- for (const excluded of EXCLUDED_PATHS) {
15536
- if (matchPath(excluded, path42)) {
15537
- return null;
15538
- }
15539
- }
15540
- const normalizedPath = path42.replace(/\/$/, "");
15541
- if (!normalizedPath.endsWith("/*") && Object.prototype.hasOwnProperty.call(ENDPOINT_CONFIGS, normalizedPath)) {
15542
- return ENDPOINT_CONFIGS[normalizedPath];
15543
- }
15544
- const sortedPatterns = Object.keys(ENDPOINT_CONFIGS).sort((a, b) => b.length - a.length);
15545
- for (const pattern of sortedPatterns) {
15546
- if (matchPath(pattern, path42)) {
15547
- return ENDPOINT_CONFIGS[pattern];
15548
- }
15549
- }
15550
- return { tier: "STANDARD" };
15551
- }
15552
16103
  function bytesToUuid(bytes) {
15553
16104
  const hex = [];
15554
16105
  for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
@@ -15596,10 +16147,11 @@ function markdownToPlainText(text) {
15596
16147
  result = result.replace(/^[-*_]{3,}$/gm, "");
15597
16148
  return result;
15598
16149
  }
15599
- var ENDPOINT_CONFIGS, EXCLUDED_PATHS, UUID_RE2, WAYAI_WORKSPACE_LAYOUT;
16150
+ var UUID_RE2, WAYAI_WORKSPACE_LAYOUT;
15600
16151
  var init_dist = __esm({
15601
16152
  "../../packages/core/dist/index.js"() {
15602
16153
  "use strict";
16154
+ init_chunk_IEWJ5MZH();
15603
16155
  init_chunk_SOJMWM63();
15604
16156
  init_chunk_WEXVYJ5H();
15605
16157
  init_chunk_LZDVDC2S();
@@ -15624,465 +16176,6 @@ var init_dist = __esm({
15624
16176
  init_chunk_L5F7ZOUJ();
15625
16177
  init_chunk_TS4EYU3W();
15626
16178
  init_chunk_R7VSUKKX();
15627
- ENDPOINT_CONFIGS = {
15628
- // OAuth callbacks - strict, fail-closed (prevent brute force)
15629
- "/oauth/callback/*": { tier: "STRICT", failClosed: true },
15630
- // Auth endpoints - strict, fail-closed
15631
- "/auth/phone": { tier: "STRICT", failClosed: true },
15632
- "/auth/mcp/exchange": { tier: "MODERATE", failClosed: true },
15633
- // Native magic-code login (pre-auth) — STRICT per-IP bounds a single host; WorkOS owns
15634
- // the per-email lockout / TTL / single-use guarantees.
15635
- "/api/auth/magic-code/*": { tier: "STRICT", failClosed: true },
15636
- // Native password login (pre-auth) — same posture as magic-code above. STRICT per-IP bounds a
15637
- // single host but FAILS OPEN on a binding error (like every IP limit); the `failClosed` flag
15638
- // here is inert on a pre-auth route (it's only enforced on the per-user branch, which needs an
15639
- // authenticated caller). The fail-CLOSED brute-force guarantee for the static credential comes
15640
- // from the route's per-email caps (burst + cumulative hourly), not this flag.
15641
- "/api/auth/password/*": { tier: "STRICT", failClosed: true },
15642
- // Every `/api/*` group whose paths carry ids pins `userLimitKey` (gh #4756), held there by
15643
- // `security/endpoint-invariants.test.ts`.
15644
- // Billing - moderate, fail-closed with user limits
15645
- "/api/billing/*": {
15646
- tier: "MODERATE",
15647
- userLimit: 100,
15648
- failClosed: true,
15649
- skipAuthenticatedUsers: true,
15650
- userLimitKey: "/api/billing/*"
15651
- },
15652
- "/webhooks/stripe": { tier: "MODERATE", failClosed: true },
15653
- "/webhooks/workos": { tier: "MODERATE", failClosed: true },
15654
- // API endpoints - standard with user limits
15655
- // 500 because the web's settings live updates refetch per hub in every open tab, whatever page
15656
- // it shows (`settingsLiveUpdateStore.ts`): one batch of changes costs about 10 + 4 per agent
15657
- // whose tools were opened + 2 per loaded resource, so a CLI push and publish (both hubs) with
15658
- // two tabs open passes 150 in a minute. A multi-file resource upload adds 2–5 per file.
15659
- "/api/setup/*": {
15660
- tier: "STANDARD",
15661
- userLimit: 500,
15662
- skipAuthenticatedUsers: true,
15663
- userLimitKey: "/api/setup/*"
15664
- },
15665
- // 500 because an eval run's drill-down (`SessionResults.tsx`) fetches its list and then every
15666
- // LLM turn's record at once: up to 201 requests (`OBSERVABILITY_LIST_CAP`, 200). Sending keeps
15667
- // the 150 it had when each path held its own budget, on the exact key below.
15668
- "/api/conversations/*": {
15669
- tier: "STANDARD",
15670
- userLimit: 500,
15671
- skipAuthenticatedUsers: true,
15672
- userLimitKey: "/api/conversations/*"
15673
- },
15674
- "/api/conversations/send-message": { tier: "STANDARD", userLimit: 150, skipAuthenticatedUsers: true },
15675
- "/api/users/*": { tier: "STANDARD", userLimit: 150, skipAuthenticatedUsers: true },
15676
- "/api/analytics/*": {
15677
- tier: "STANDARD",
15678
- userLimit: 150,
15679
- skipAuthenticatedUsers: true,
15680
- userLimitKey: "/api/analytics/*"
15681
- },
15682
- // 500 because `wayai eval-results --eval` reads completed sessions one by one, newest first,
15683
- // until one holds the eval (up to the 1000 the list returns). Tenant SQL keeps the 150 it had
15684
- // when each path held its own budget: each query may run 30 s on shared-tenant ClickHouse
15685
- // after its caller has gone (`sql-validator.ts`, stage 10b).
15686
- "/api/evals/*": {
15687
- tier: "STANDARD",
15688
- userLimit: 500,
15689
- skipAuthenticatedUsers: true,
15690
- userLimitKey: "/api/evals/*"
15691
- },
15692
- "/api/evals/sql": { tier: "STANDARD", userLimit: 150, skipAuthenticatedUsers: true },
15693
- // An eval run's calls (gh #4418), kept off the end user's call budgets below. Create keeps the
15694
- // 150 it had: it opens a provider session on the org's own key, billed per second. The routes
15695
- // on one call take a budget of their own so no eval read can spend it: a refused hang-up
15696
- // leaves the call running, and billing. 500 because a call suite polls each run's record
15697
- // every second, about 64 requests a minute per suite.
15698
- "/api/evals/calls": { tier: "STANDARD", userLimit: 150, skipAuthenticatedUsers: true },
15699
- "/api/evals/calls/*": {
15700
- tier: "STANDARD",
15701
- userLimit: 500,
15702
- skipAuthenticatedUsers: true,
15703
- userLimitKey: "/api/evals/calls/*"
15704
- },
15705
- "/api/files/*": { tier: "STANDARD", userLimit: 150, skipAuthenticatedUsers: true },
15706
- // The org contact book (wayai#4931 PR 2b). A CSV import sends one request per
15707
- // `CONTACT_IMPORT_BATCH_MAX` rows, so 150 a minute imports 75,000 rows a minute.
15708
- "/api/contact-book/*": {
15709
- tier: "STANDARD",
15710
- userLimit: 150,
15711
- skipAuthenticatedUsers: true,
15712
- userLimitKey: "/api/contact-book/*"
15713
- },
15714
- // An automation email's one-click unsubscribe (wayai#4931 PR 2d) — public: its token is its one
15715
- // credential, so this per-IP limit is its one bound, and runs before the route reads anything.
15716
- // MODERATE (30/min per IP), as the community email's unsubscribe, for the same reason: mail
15717
- // clients' one-click unsubscribe POSTs it from their own servers, which many recipients' clicks
15718
- // share. An exact key, so it wins over the wildcard above.
15719
- [CONTACT_BOOK_EMAIL_UNSUBSCRIBE_PATH]: { tier: "MODERATE" },
15720
- "/api/automations/*": {
15721
- tier: "STANDARD",
15722
- userLimit: 150,
15723
- skipAuthenticatedUsers: true,
15724
- userLimitKey: "/api/automations/*"
15725
- },
15726
- // A hub's own contact lists (wayai#4931 PR 2c): config edits, at the automations' rate.
15727
- "/api/hub-contact-lists/*": {
15728
- tier: "STANDARD",
15729
- userLimit: 150,
15730
- skipAuthenticatedUsers: true,
15731
- userLimitKey: "/api/hub-contact-lists/*"
15732
- },
15733
- "/api/message-templates/*": {
15734
- tier: "STANDARD",
15735
- userLimit: 150,
15736
- skipAuthenticatedUsers: true,
15737
- userLimitKey: "/api/message-templates/*"
15738
- },
15739
- "/api/live-updates/*": { tier: "STANDARD", userLimit: 150, skipAuthenticatedUsers: true },
15740
- "/api/hub-users/*": {
15741
- tier: "STANDARD",
15742
- userLimit: 150,
15743
- skipAuthenticatedUsers: true,
15744
- userLimitKey: "/api/hub-users/*"
15745
- },
15746
- "/api/conversation-state/*": {
15747
- tier: "STANDARD",
15748
- userLimit: 150,
15749
- skipAuthenticatedUsers: true,
15750
- userLimitKey: "/api/conversation-state/*"
15751
- },
15752
- // The authenticated byte route (`routes/files/r2.ts`), mounted after its `authMiddleware` so
15753
- // the caller is known: each request reads the HubDO its key names, and a hub id never created
15754
- // still wakes a Durable Object (gh #4747; since gh #4775 T5 one that writes nothing). The key is pinned
15755
- // because the paths are file keys, which would otherwise each get a fresh budget (and one
15756
- // over 256 characters would be refused outright, `checkUserLimitNative`).
15757
- //
15758
- // 500 a minute because the web chat fetches every attachment of every loaded message at
15759
- // once, with no list virtualization: up to 50 per 50-message page, 200 per ended
15760
- // conversation or history entry at one file per message, and 4 per message at most in its
15761
- // image grid. Repeats come from the browser's HTTP cache (`private, max-age=3600`); mobile
15762
- // renders through a disk cache and plays audio from `/files/signed`. The budget is shared by
15763
- // a caller's tabs and devices. The other two byte routes, `/files/signed/*` and
15764
- // `/files/public/*`, mount no limiter (the reason is at their entries in
15765
- // `security/endpoint-invariants.test.ts`'s route registry).
15766
- "/files/auth/*": {
15767
- tier: "STANDARD",
15768
- userLimit: 500,
15769
- skipAuthenticatedUsers: true,
15770
- userLimitKey: "/files/auth/*"
15771
- },
15772
- // Channel webhooks - high throughput (external systems, never authenticated)
15773
- "/webhooks/whatsapp/*": { tier: "HIGH" },
15774
- "/webhooks/instagram/*": { tier: "HIGH" },
15775
- // An automation's webhook deliveries (wayai#4931 D13) — no bearer token: the automation's
15776
- // secret is the credential, checked against one HubDO read. This per-IP limit runs first, before
15777
- // anything is read. HIGH, as the channel webhooks: senders are servers, and an integration
15778
- // platform's egress addresses carry many tenants' deliveries. The per-automation budget is
15779
- // `AUTOMATION_WEBHOOK_PER_AUTOMATION_LIMIT`, counted after the delivery is verified.
15780
- [`${AUTOMATION_WEBHOOK_MOUNT}/*`]: { tier: "HIGH" },
15781
- // Reports (CLI → platform_report triage queue). One per-user budget covers intake
15782
- // (POST), the content edit (PATCH /:id), the reporter read-back (GET, GET /:id), and the
15783
- // verification loop (POST /:id/accept, /:id/contest). The limiter is path-based, so a
15784
- // method-level split (loose reads, strict writes) isn't expressible — GET /:id and PATCH
15785
- // /:id share a path. So the budget is sized for the read-heavy poll loop (a reporter agent
15786
- // polling `report get`/`list` while it waits for the fix), at 60/hour. Intake-flooding stays
15787
- // bounded regardless: identical reports collapse on `fingerprint` (ON CONFLICT DO NOTHING),
15788
- // so a runaway create loop merges rather than floods. Still STRICT tier + failClosed so a
15789
- // genuinely runaway distinct-report loop surfaces as 429. The `/*` is load-bearing — an
15790
- // exact `/api/reports` key would leave the `/:id` paths unmatched (→ unthrottled).
15791
- // `userLimitKey` is equally load-bearing: without it, middleware falls back to each request's
15792
- // path and report IDs/actions receive independent counters instead of this shared budget.
15793
- "/api/reports/*": {
15794
- tier: "STRICT",
15795
- userLimit: 60,
15796
- userWindow: 3600,
15797
- failClosed: true,
15798
- skipAuthenticatedUsers: true,
15799
- userLimitKey: "/api/reports/*"
15800
- },
15801
- // Admin report mutations (transition/group/ungroup/edit) — platform-admin
15802
- // gated at the route, rate-limited here as defense-in-depth. Generous STANDARD
15803
- // cap so legitimate triage bursts aren't throttled (grouping is one request
15804
- // even for many members); deliberately NOT failClosed so a Redis blip never
15805
- // blocks an admin's triage. The bare GET reads share this budget harmlessly.
15806
- "/api/admin/reports/*": { tier: "STANDARD", userLimit: 150, userLimitKey: "/api/admin/reports/*" },
15807
- // GitHub webhook (issues.closed → platform_report addressed). MODERATE +
15808
- // fail-closed; GitHub retries on transient failures so a brief Redis blip
15809
- // doesn't lose state.
15810
- "/webhooks/github": { tier: "MODERATE", failClosed: true },
15811
- // Account deletion — strict on the destructive POST + public undo. The
15812
- // /api/users/* default tier above ALSO matches these, but `getEndpointConfig`
15813
- // returns the longest-prefix match, so the more specific entries here win.
15814
- // `skipAuthenticatedUsers` deliberately omitted: every caller is authenticated,
15815
- // so including it would make `userLimit` a no-op — the per-user cap IS the gate.
15816
- "/api/users/me/deletion": { tier: "STRICT", userLimit: 3, userWindow: 3600, failClosed: true },
15817
- "/api/users/me/deletion/preflight": { tier: "STANDARD", userLimit: 30, failClosed: true },
15818
- "/api/users/deletion/undo": { tier: "STRICT", failClosed: true },
15819
- // Production-base destruction.
15820
- //
15821
- // Keyed off the CONTRACT constants, not literals: `BASE_DESTRUCTION_CONFIRM_PATH`'s own
15822
- // docstring promises that the mount, the `AUTH_BYPASS_PATHS` entry, the rate-limit key and
15823
- // the URL the email embeds move together, and a literal here is exactly how that promise
15824
- // goes quietly false — leaving an unauthenticated HMAC endpoint on the default tier.
15825
- //
15826
- // The emailed GET is PUBLIC, so it gets the IP-tier bound the pre-auth routes get.
15827
- // `failClosed` is deliberately NOT set: it is only consulted on the per-USER branch, which
15828
- // needs an authenticated caller, so on a pre-auth route it is inert — the same thing
15829
- // `/api/auth/magic-code/send` above says about itself. The IP limiter fails OPEN on a
15830
- // limiter error by design (`packages/runtime/src/rate-limiter.ts`), and claiming otherwise
15831
- // would tell the next auditor a protection exists that does not.
15832
- [BASE_DESTRUCTION_CONFIRM_PATH]: { tier: "STRICT" },
15833
- // The console's read, which every production base's settings pane issues on mount. Without
15834
- // its own entry it inherits the mount-wide budget below, and an admin clicking through
15835
- // thirty bases in an hour would be 429'd by a limit sized for irreversible writes.
15836
- [`${BASE_DESTRUCTION_PREFIX}/destructions`]: { tier: "STANDARD", userLimit: 150 },
15837
- // The mutating routes: initiate, the authenticated confirm, and cancel. Per user rather than
15838
- // per IP — all three are authenticated and org-admin gated — and hourly, since a single human
15839
- // touching thirty production-deletion operations in an hour is already far past normal.
15840
- // Longest-prefix wins, so the two entries above are not widened by this one. The key is
15841
- // pinned so those thirty are counted across every base the caller touches (gh #4756).
15842
- [`${BASE_DESTRUCTION_PREFIX}/*`]: {
15843
- tier: "STANDARD",
15844
- userLimit: 30,
15845
- userWindow: 3600,
15846
- failClosed: true,
15847
- userLimitKey: `${BASE_DESTRUCTION_PREFIX}/*`
15848
- },
15849
- // Email-invite preview — public, fail-closed. MODERATE (30/min IP) leaves
15850
- // headroom for the signup-page server-fetch + reloads + prefetches; STRICT
15851
- // would throttle legitimate users opening the link in multiple tabs.
15852
- "/api/invites/preview": { tier: "MODERATE", failClosed: true },
15853
- // "Don't email me again" (gh #4661) — public; one POST per click on its confirm page. STRICT
15854
- // per IP. No `failClosed`: it is consulted only on the per-user branch, which a pre-auth route
15855
- // never reaches (see `BASE_DESTRUCTION_CONFIRM_PATH` above).
15856
- "/api/invites/unsubscribe": { tier: "STRICT" },
15857
- // Admin DO debug reads — the ONE deliberate deviation from the Data Explorer
15858
- // catch-all below, and the only reason a per-route key still exists here.
15859
- // Platform-admin is enforced at the route; this is the independent
15860
- // defense-in-depth signal so a runaway investigation script surfaces as 429.
15861
- //
15862
- // STRICT is kept ON PURPOSE even though the catch-all moved off it: per IP, at 5/min vs
15863
- // STANDARD's 100/min, walking 10k ids takes ~33h instead of ~100min — a detection window
15864
- // worth keeping on the routes that return RAW DO ROWS.
15865
- //
15866
- // The cost, stated rather than elided: STRICT is one bucket shared with
15867
- // `/oauth/callback/*` and `/api/auth/magic-code/*`, so a debug session from a
15868
- // NAT'd office IP can 429 a colleague's login for up to a minute. That trade is
15869
- // accepted for a rare, deliberate investigation surface — and is exactly why the
15870
- // browse routes below are NOT here.
15871
- //
15872
- // The key is pinned, so a sweep of `/debug/do/hub/<id>/tables` across ids spends one
15873
- // per-admin budget (gh #4756). 120 an hour: an agent's investigation of a dozen
15874
- // conversations made 37 debug reads in its busiest hour.
15875
- "/api/admin/data-explorer/debug/*": {
15876
- tier: "STRICT",
15877
- userLimit: 120,
15878
- userWindow: 3600,
15879
- failClosed: true,
15880
- userLimitKey: "/api/admin/data-explorer/debug/*"
15881
- },
15882
- // Catch-all for the rest of the Data Explorer (`/search`, `/entity/*`, `/do/*`,
15883
- // `/kv/*`, the DELETE routes). Added as a catch-all rather than another per-route
15884
- // key because this budget kept lagging behind routes joining the debug boundary:
15885
- // with no match the path falls to the default, which sets no `userLimit`, so the
15886
- // per-user layer never runs at all. `getEndpointConfig` takes the longest match,
15887
- // so `debug/*` above still wins.
15888
- //
15889
- // `/entity/*` deliberately has NO key of its own: it needs precisely this config,
15890
- // and a duplicate entry is how the two drift apart on the next edit.
15891
- //
15892
- // `tier` is deliberately STANDARD, not STRICT. `tier` is the per-IP knob and
15893
- // STRICT is 5/min keyed on IP ALONE — one bucket shared across every STRICT
15894
- // endpoint. On these browse routes that breaks the console (open Browse KV and
15895
- // inspect four values = 6 requests; expand six org rows in the search results =
15896
- // 6 `/entity/org/:id` calls) and would drain the same IP's budget for
15897
- // `/oauth/callback/*` and `/api/auth/magic-code/*`, which on a NAT'd office IP
15898
- // means all admins. The per-user hourly cap is the leg that matters here.
15899
- //
15900
- // The canonical key is deliberate: request paths contain DO ids and arbitrary
15901
- // malformed requests can contain credential material. Keying on the request path
15902
- // both stores that material in Redis/logs and gives every target a fresh counter.
15903
- // One stable identity makes every browse request consume the same per-admin
15904
- // budget without storing request targets.
15905
- //
15906
- // 600/hour preserves the original pagination requirement (about 30k rows at
15907
- // 50/page) while now genuinely bounding id/key enumeration across the surface.
15908
- "/api/admin/data-explorer/*": {
15909
- tier: "STANDARD",
15910
- userLimit: 600,
15911
- userWindow: 3600,
15912
- failClosed: true,
15913
- userLimitKey: "/api/admin/data-explorer/*"
15914
- },
15915
- // Admin harness incident-response (harness-agents PR 2.9b) — mass-destroy reaps
15916
- // EVERY live harness session/token/slot in scope, so it gets a strict budget
15917
- // (STRICT + a low per-user hourly cap + fail-closed). Platform-admin is enforced
15918
- // at the route; this is the independent defense-in-depth signal so a runaway
15919
- // reap loop surfaces as 429. The break-glass disable/enable rides through
15920
- // `/api/admin/config` (its own STANDARD budget), not this key.
15921
- "/api/admin/harness/*": {
15922
- tier: "STRICT",
15923
- userLimit: 10,
15924
- userWindow: 3600,
15925
- failClosed: true,
15926
- userLimitKey: "/api/admin/harness/*"
15927
- },
15928
- // Platform-wide team-orphan repair — a single call can WRITE across every hub of
15929
- // every org (HubDO purge + UserDO mirror fan-out), so it gets the same strict,
15930
- // fail-closed budget as the other cross-platform admin write. Exact path (not a
15931
- // prefix): the sibling `/api/admin/hubs/:hubId/harness` flag flips stay on the
15932
- // default admin budget. Platform-admin is enforced at the route; this is the
15933
- // defense-in-depth signal that a repair loop surfaces as 429.
15934
- "/api/admin/hubs/repair-team-orphans": { tier: "STRICT", userLimit: 10, userWindow: 3600, failClosed: true },
15935
- // Branching-orphan repair (#4577) — deletes named production rows, and its report mode
15936
- // fans out over every production hub of every org, so it takes the same strict,
15937
- // fail-closed budget as the team-orphan repair. Exact path, for the same reason.
15938
- "/api/admin/hubs/repair-branching-orphans": { tier: "STRICT", userLimit: 10, userWindow: 3600, failClosed: true },
15939
- // CI/GitOps endpoints - standard with user limits. 500 because `wayai push` makes four requests
15940
- // (pull, diff, push, pull) and a GitOps job may push every hub at once with one token; when
15941
- // each path held its own 100, that ran to 100 hubs a minute.
15942
- "/api/ci/*": { tier: "STANDARD", userLimit: 500, skipAuthenticatedUsers: true, userLimitKey: "/api/ci/*" },
15943
- // Attested `/v1` proxy onto the Rekor Worker. `userLimitKey` is load-bearing here: the
15944
- // per-user budget defaults to the full request path, and this surface's paths carry base,
15945
- // record, and file ids — so without a pinned key every distinct resource would get its own
15946
- // fresh budget and the per-user limit would bound nothing. One shared budget across the
15947
- // whole data surface is the intent.
15948
- [`${DATA_PROXY_PREFIX}/*`]: {
15949
- tier: "STANDARD",
15950
- userLimit: 150,
15951
- skipAuthenticatedUsers: true,
15952
- userLimitKey: `${DATA_PROXY_PREFIX}/*`
15953
- },
15954
- // Voice calls — the end user's call routes (`routes/api/calls`). Every caller is signed
15955
- // in, so the per-IP tier is skipped for them: Brazilian mobile carriers put many callers
15956
- // behind one IP, and a shared bucket would refuse one caller for another's traffic.
15957
- //
15958
- // Create is the only route that spends: it opens a provider session on the org's own
15959
- // key, billed per second. So it takes its own exact key and an hourly per-user cap, which
15960
- // lives on Redis and is fail-closed like the other hourly caps. The exact key wins over
15961
- // the wildcard below, which also matches `/api/calls` (see `getEndpointConfig`).
15962
- "/api/calls": {
15963
- tier: "STANDARD",
15964
- userLimit: 30,
15965
- userWindow: 3600,
15966
- failClosed: true,
15967
- skipAuthenticatedUsers: true,
15968
- userLimitKey: "/api/calls"
15969
- },
15970
- // The routes on one call: `ready`, hang-up, the delegation report and the status read; and
15971
- // the opening read (`/api/calls/openings/:language`), one per call, before it exists.
15972
- // NEVER fail-closed: a refused hang-up leaves the call running, and billing, until a
15973
- // limit ends it. The 60 s budget runs on the native binding, which fails open. The key is
15974
- // pinned because the paths carry the call id, which would otherwise give every call a
15975
- // fresh budget.
15976
- "/api/calls/*": {
15977
- tier: "STANDARD",
15978
- userLimit: 150,
15979
- skipAuthenticatedUsers: true,
15980
- userLimitKey: "/api/calls/*"
15981
- },
15982
- // Contacts between accounts (gh #4661, `routes/api/contacts`). Every caller is signed in, so
15983
- // the per-IP tier is skipped. The keys are pinned because the paths carry user and invite
15984
- // ids, which would otherwise give each id a fresh budget. Blocks take their own, lower
15985
- // budget (#4661 Limits: blocks are rate-limited). The invites-per-day cap and every list
15986
- // cap are counted in the caller's UserDO, in the same write they bound, not here.
15987
- "/api/contacts/*": {
15988
- tier: "STANDARD",
15989
- userLimit: 60,
15990
- skipAuthenticatedUsers: true,
15991
- userLimitKey: "/api/contacts/*"
15992
- },
15993
- "/api/contacts/blocks/*": {
15994
- tier: "STANDARD",
15995
- userLimit: 30,
15996
- skipAuthenticatedUsers: true,
15997
- userLimitKey: "/api/contacts/blocks/*"
15998
- },
15999
- // Person-to-person threads (gh #4661, `routes/api/threads`): the per-user send limit, 60 a
16000
- // minute on the native binding. Every thread route shares the one budget, reads included:
16001
- // the paths carry thread and message ids, so the key is pinned, and a pattern here cannot
16002
- // tell a send from a read of the same path. Like every 60 s budget it fails open on a
16003
- // binding error, so it bounds sends in the steady state only.
16004
- "/api/threads/*": {
16005
- tier: "STANDARD",
16006
- userLimit: 60,
16007
- skipAuthenticatedUsers: true,
16008
- userLimitKey: "/api/threads/*"
16009
- },
16010
- // An org admin's erasure of one person's thread messages (gh #4661 owner decision 7): any user
16011
- // can own a free org, so each request arms that org's alarm, and this caps how fast one person
16012
- // can ask. The org's own daily cap (`PEOPLE_ORG_ERASURES_PER_DAY`, counted in its OrgDO in the
16013
- // write it bounds) is the fail-closed bound; this budget, on the native binding, fails open.
16014
- "/api/threads/org-erasures": {
16015
- tier: "STANDARD",
16016
- userLimit: 10,
16017
- skipAuthenticatedUsers: true,
16018
- userLimitKey: "/api/threads/org-erasures"
16019
- },
16020
- // A person's report of a message, thread or person (`people-reports.ts`): each one written
16021
- // arms the PlatformDO's alarm and emails the platform admins, so it takes its own, lower
16022
- // budget. The reporter's daily cap (`PEOPLE_REPORTS_PER_DAY`, counted in the report store in
16023
- // the write it bounds) is the fail-closed bound; this budget, on the native binding, fails open.
16024
- "/api/threads/reports": {
16025
- tier: "STANDARD",
16026
- userLimit: 10,
16027
- skipAuthenticatedUsers: true,
16028
- userLimitKey: "/api/threads/reports"
16029
- },
16030
- // Communities (gh #4872, `routes/api/communities`): every signed-in route under the mount (the
16031
- // preview below excepted) shares one per-user budget, which is also each member's send limit and
16032
- // bounds the reads that scan a community's rows (a title search, the stats); a community's own
16033
- // limits (its rate, the daily caps, slow mode) are counted in its CommunityDO, in the write they
16034
- // bound. Every caller is signed in, so the per-IP tier is skipped; the key is pinned because the
16035
- // paths carry community, room and message ids.
16036
- "/api/communities/*": {
16037
- tier: "STANDARD",
16038
- userLimit: 60,
16039
- skipAuthenticatedUsers: true,
16040
- userLimitKey: "/api/communities/*"
16041
- },
16042
- // The join page's preview of a link — public, fetched from the visitor's browser, so this
16043
- // per-IP limit is its one bound and runs before the route reads anything (#4872
16044
- // owner-delegated decision 9). MODERATE (30/min per IP) leaves room for reloads and several
16045
- // tabs. An exact key, so it wins over the wildcard above.
16046
- "/api/communities/join-preview": { tier: "MODERATE" },
16047
- // An announcement email's unsubscribe (#4872 PR 8a) — public: its token is its one credential, so
16048
- // this per-IP limit is its one bound, and runs before the route reads anything. MODERATE (30/min
16049
- // per IP), not STRICT: mail clients' one-click unsubscribe POSTs it from their own servers, which
16050
- // many members' clicks share, and STRICT's 5/min bucket is shared with sign-in. An exact key, so
16051
- // it wins over the wildcard above.
16052
- [COMMUNITY_EMAIL_UNSUBSCRIBE_PATH]: { tier: "MODERATE" },
16053
- // The WayAI Guide's help entry (wayai#4948, `routes/api/guide.ts`): one POST per click by a viewer
16054
- // the snapshot does not show enrolled, which reads the person's row on the Guide hub and writes only
16055
- // what is missing (an enrolled viewer's click opens the hub without a request). Every caller is
16056
- // signed in, so the per-IP tier is skipped; 10 a minute per person on the native binding (fails
16057
- // open, like every 60 s budget) bounds what a looping client costs the one Guide HubDO.
16058
- "/api/guide/access": {
16059
- tier: "STANDARD",
16060
- userLimit: 10,
16061
- skipAuthenticatedUsers: true,
16062
- userLimitKey: "/api/guide/access"
16063
- },
16064
- // App channel - high throughput with user limits
16065
- "/channels/app/*": { tier: "HIGH", userLimit: 500, skipAuthenticatedUsers: true },
16066
- // API channel (api-channel-connector) - machine-to-machine, high throughput
16067
- "/channels/api/*": { tier: "HIGH", userLimit: 500, skipAuthenticatedUsers: true },
16068
- // System/scheduled - high throughput (internal)
16069
- "/channels/system/*": { tier: "HIGH", skipAuthenticatedUsers: true },
16070
- "/scheduled/*": { tier: "HIGH", skipAuthenticatedUsers: true },
16071
- // WebSocket - standard with user limit to prevent connection spam
16072
- "/live-updates/ws": { tier: "STANDARD", userLimit: 100, failClosed: true },
16073
- // A community's live socket (gh #4872 PR 5, `GET /communities/:communityId/live`): outside
16074
- // `/api/*`, with no bearer token, so its per-IP tier is its one bound before the CommunityDO
16075
- // checks the ticket. STANDARD, as `/live-updates/ws`: a client opens one per community it shows
16076
- // and reconnects within the hour.
16077
- "/communities/*": { tier: "STANDARD" },
16078
- // WS ticket mint - per-user 60/min accommodates aggressive reconnect patterns
16079
- // without opening abuse vectors; fail-closed so Redis outages don't let
16080
- // runaway clients mint unbounded tickets.
16081
- "/api/auth/ws-ticket": { tier: "MODERATE", userLimit: 60, failClosed: true, skipAuthenticatedUsers: true },
16082
- "/api/auth/session-check": { tier: "STANDARD", userLimit: 120, skipAuthenticatedUsers: true },
16083
- "/api/auth/logout": { tier: "MODERATE", userLimit: 10, failClosed: true, skipAuthenticatedUsers: true }
16084
- };
16085
- EXCLUDED_PATHS = /* @__PURE__ */ new Set(["/health"]);
16086
16179
  UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16087
16180
  WAYAI_WORKSPACE_LAYOUT = {
16088
16181
  /** Product-namespaced root dir at the git root. */
@@ -33235,7 +33328,7 @@ token (\`wayai login --token\`, or WAYAI_TOKEN) is refused.
33235
33328
  var init_rekor_toolset = __esm({
33236
33329
  "../../packages/core/dist/contracts/rekor-toolset.js"() {
33237
33330
  "use strict";
33238
- init_chunk_BVQE66I2();
33331
+ init_chunk_4MRBBR5L();
33239
33332
  init_chunk_BEOHBREG();
33240
33333
  init_chunk_DHPUPI4Y();
33241
33334
  }
@@ -34200,7 +34293,10 @@ var init_plan = __esm({
34200
34293
 
34201
34294
  // src/knowledge/migrate.ts
34202
34295
  import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
34203
- async function resolveBases(kb, baseId2) {
34296
+ function refreshPreviewCommand(orgId, base) {
34297
+ return `wayai knowledge refresh-preview --org ${orgId} --base ${safe(base)}`;
34298
+ }
34299
+ async function resolveBases(kb, baseId2, orgId) {
34204
34300
  const base = await kb.getBase(baseId2);
34205
34301
  if (!base || base.deleted) throw expected(`Base "${safe(baseId2)}" not found in this organization.`);
34206
34302
  if (base.environment === "preview") {
@@ -34220,7 +34316,7 @@ async function resolveBases(kb, baseId2) {
34220
34316
  }
34221
34317
  if (previews.length !== 1) {
34222
34318
  throw expected(
34223
- previews.length === 0 ? `Base "${safe(baseId2)}" has no preview to write its file types in. Create one with \`wayai bases create-preview ${safe(baseId2)} --name <name>\`.` : `Base "${safe(baseId2)}" has ${previews.length} previews (${previews.map(safe).join(", ")}). Pass the one to write its file types in as --base.`
34319
+ previews.length === 0 ? `Base "${safe(baseId2)}" has no preview. Create one with \`wayai bases create-preview ${safe(baseId2)} --name <name>\`, then copy production's files into it with \`${refreshPreviewCommand(orgId, baseId2)}\`.` : `Base "${safe(baseId2)}" has ${previews.length} previews (${previews.map(safe).join(", ")}). Pass the one to use as --base.`
34224
34320
  );
34225
34321
  }
34226
34322
  return { production: base.id, preview: previews[0] };
@@ -34326,7 +34422,7 @@ function printNotes(io, notes) {
34326
34422
  }
34327
34423
  }
34328
34424
  async function migrateKnowledge(reader, kb, opts, io) {
34329
- const { production: productionId, preview } = await resolveBases(kb, opts.base);
34425
+ const { production: productionId, preview } = await resolveBases(kb, opts.base, opts.orgId);
34330
34426
  const hubs = opts.hubId === void 0 ? [] : await reader.listHubs();
34331
34427
  const hub = opts.hubId === void 0 ? void 0 : productionHub(hubs, opts.hubId);
34332
34428
  const production = await Production.read(kb, productionId);
@@ -34465,7 +34561,7 @@ Dry run: nothing written (${writes === 0 ? "nothing to write" : `${writes} write
34465
34561
  for (const file of creates) await kb.putFile(productionId, file, await bodyBytes(reader, file));
34466
34562
  io.log(
34467
34563
  `
34468
- Done: ${toWrite.length} file type(s) written, ${toPromote.length} promoted, ${creates.length} file(s) written. The preview holds none of these files.`
34564
+ Done: ${toWrite.length} file type(s) written, ${toPromote.length} promoted, ${creates.length} file(s) written. The preview holds none of these files: copy them with \`${refreshPreviewCommand(opts.orgId, opts.base)}\`.`
34469
34565
  );
34470
34566
  return { ...result, written: true };
34471
34567
  }
@@ -34635,6 +34731,9 @@ function knowledgeBase(client) {
34635
34731
  }
34636
34732
  throw expected(`The listing of file type "${fileType}" on base "${base}" did not end after ${MAX_LIST_PAGES} pages.`);
34637
34733
  },
34734
+ // `requestRaw`: the page is read whole, `{ data: … }`, as WayAI's copy of its answer declares it
34735
+ // (`@wayai/core/contracts` `rekorFileSnapshotAnswerSchema`).
34736
+ snapshotPage: (preview, cursor) => pacer.run(() => client.requestRaw("POST", `/v1/${baseSeg(preview)}/files/snapshot`, cursor === void 0 ? {} : { cursor })),
34638
34737
  async putFile(base, file, bytes) {
34639
34738
  await pacer.run(
34640
34739
  () => client.upload(
@@ -34667,6 +34766,37 @@ var init_remote = __esm({
34667
34766
  }
34668
34767
  });
34669
34768
 
34769
+ // src/knowledge/refresh-preview.ts
34770
+ async function refreshPreviewKnowledge(kb, opts, io) {
34771
+ const { production, preview } = await resolveBases(kb, opts.base, opts.orgId);
34772
+ const question = `Make the files of preview "${sanitizeTerminalText(preview)}" equal production "${sanitizeTerminalText(production)}"'s? Files the preview changed are replaced, and files only it holds are removed.`;
34773
+ if (!opts.yes && !await io.confirm(question)) {
34774
+ io.log("Cancelled.");
34775
+ return null;
34776
+ }
34777
+ io.log(`Copying the files of production base "${sanitizeTerminalText(production)}" into its preview "${sanitizeTerminalText(preview)}".`);
34778
+ const run = await runFileSnapshot({
34779
+ sendPage: (cursor) => kb.snapshotPage(preview, cursor),
34780
+ // The data product refuses a page whose preview file changed while it was being copied, and
34781
+ // asks for the same cursor again.
34782
+ isConflict: (error) => error instanceof ApiError && error.status === FILE_SNAPSHOT_CONFLICT_STATUS,
34783
+ onPage: (page, n) => io.log(` Page ${n}: ${page.written} written, ${page.removed} removed, ${page.unchanged} unchanged.`)
34784
+ });
34785
+ io.log(
34786
+ !fileSnapshotChanged(run) ? `Nothing changed in ${run.pages} page(s): the preview's ${run.unchanged} file(s) already equal production's.` : `Done in ${run.pages} page(s): ${run.written} written, ${run.removed} removed, ${run.unchanged} unchanged. The preview's files now equal production's.`
34787
+ );
34788
+ return run;
34789
+ }
34790
+ var init_refresh_preview = __esm({
34791
+ "src/knowledge/refresh-preview.ts"() {
34792
+ "use strict";
34793
+ init_contracts();
34794
+ init_api_client();
34795
+ init_terminal_output();
34796
+ init_migrate2();
34797
+ }
34798
+ });
34799
+
34670
34800
  // src/commands/knowledge.ts
34671
34801
  var knowledge_exports = {};
34672
34802
  __export(knowledge_exports, {
@@ -34688,15 +34818,23 @@ function readArgs(args2) {
34688
34818
  function parseKnowledgeArgs(args2) {
34689
34819
  const { values, positionals } = readArgs(args2);
34690
34820
  const [verb, ...extra] = positionals;
34691
- if (verb !== "migrate") throw usageError3(verb === void 0 ? "No subcommand given." : `Unknown subcommand ${JSON.stringify(verb)}.`);
34821
+ if (verb !== "migrate" && verb !== "refresh-preview") {
34822
+ throw usageError3(verb === void 0 ? "No subcommand given." : `Unknown subcommand ${JSON.stringify(verb)}.`);
34823
+ }
34692
34824
  if (extra.length > 0) throw usageError3(`Unexpected argument ${JSON.stringify(extra[0])}.`);
34693
34825
  if (!values.org) throw usageError3("--org <org_id> is required.");
34694
34826
  if (!UUID_RE3.test(values.org)) throw usageError3(`Invalid --org ${JSON.stringify(values.org)}: expected an organization id (a UUID).`);
34695
34827
  if (!values.base) throw usageError3("--base <knowledge base> is required.");
34828
+ if (verb === "refresh-preview") {
34829
+ if (values.hub !== void 0) throw usageError3("--hub is migrate's: a refresh copies the whole knowledge base.");
34830
+ if (values["dry-run"] !== void 0) throw usageError3("--dry-run is migrate's: a refresh has none.");
34831
+ return { verb, orgId: values.org, base: values.base, yes: values.yes === true };
34832
+ }
34696
34833
  if (values.hub !== void 0 && !UUID_RE3.test(values.hub)) {
34697
34834
  throw usageError3(`Invalid --hub ${JSON.stringify(values.hub)}: expected a hub id (a UUID).`);
34698
34835
  }
34699
34836
  return {
34837
+ verb,
34700
34838
  orgId: values.org,
34701
34839
  base: values.base,
34702
34840
  hubId: values.hub,
@@ -34710,6 +34848,12 @@ async function knowledgeCommand(args2) {
34710
34848
  return;
34711
34849
  }
34712
34850
  const options = parseKnowledgeArgs(args2);
34851
+ if (options.verb === "refresh-preview") {
34852
+ if (!options.yes && !isInteractive()) throw usageError3("It asks before it copies; pass -y when not running in a terminal.");
34853
+ const data2 = await createDataClient(options.orgId, { refreshLogin: true });
34854
+ await refreshPreviewKnowledge(knowledgeBase(data2), options, { log: (line) => console.log(line), confirm });
34855
+ return;
34856
+ }
34713
34857
  if (!options.dryRun && !options.yes && !isInteractive()) {
34714
34858
  throw usageError3("It asks before it writes; pass -y when not running in a terminal, or --dry-run.");
34715
34859
  }
@@ -34733,11 +34877,15 @@ var init_knowledge = __esm({
34733
34877
  init_helpers();
34734
34878
  init_migrate2();
34735
34879
  init_remote();
34880
+ init_refresh_preview();
34736
34881
  USAGE4 = `
34737
- wayai knowledge migrate \u2014 move knowledge resources into the organization's knowledge base
34882
+ wayai knowledge \u2014 the organization's knowledge base
34738
34883
 
34739
34884
  Usage:
34740
34885
  wayai knowledge migrate --org <org_id> --base <knowledge base> [--hub <hub_id>] [--dry-run] [-y]
34886
+ wayai knowledge refresh-preview --org <org_id> --base <knowledge base> [-y]
34887
+
34888
+ migrate: move knowledge resources into the organization's knowledge base.
34741
34889
 
34742
34890
  Without --hub, it moves the organization's knowledge resources; with --hub, that hub's own
34743
34891
  (read from its production hub), once the organization's have moved. Each resource becomes a
@@ -34748,6 +34896,15 @@ Usage:
34748
34896
  written in production.
34749
34897
  --dry-run List what it would write, and every reason it would stop, without writing.
34750
34898
  -y, --yes Skip the question before writing.
34899
+
34900
+ refresh-preview: make the knowledge base's preview hold its production base's files \u2014 after
34901
+ creating the preview, and whenever it should be current again. Files the preview changed are
34902
+ replaced and files only it holds are removed; nothing else on it changes, and production is
34903
+ only read. It prints what each page copied, removed and left alone; a re-run of a current
34904
+ preview changes nothing.
34905
+
34906
+ --base The production knowledge base, or its preview.
34907
+ -y, --yes Skip the question before copying.
34751
34908
  `;
34752
34909
  OPTIONS = {
34753
34910
  org: { type: "string" },
@@ -41278,6 +41435,7 @@ Commands:
41278
41435
  communities An organization's communities: list, join link (get/rotate/off), stats, access list (list/add/remove) \u2014 \`wayai communities --help\`
41279
41436
  contacts Link a contact to its customer record in a base, or unlink it (browser sign-in only) \u2014 \`wayai contacts --help\`
41280
41437
  knowledge migrate Move an organization's knowledge resources, then a hub's (--hub), into its knowledge base as file types and files \u2014 \`wayai knowledge --help\`
41438
+ knowledge refresh-preview Make the knowledge base's preview hold its production base's files \u2014 \`wayai knowledge --help\`
41281
41439
  report create Report a platform bug (submits to the triage queue)
41282
41440
  report edit Amend your own pending report (title/description/error/steps/context)
41283
41441
  update Update CLI to the latest version