create-qpq-app 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +4 -4
  2. package/lib/commonjs/steps/013_printNextSteps.js +2 -2
  3. package/lib/commonjs/steps/013_printNextSteps.js.map +1 -1
  4. package/lib/esm/steps/013_printNextSteps.js +2 -2
  5. package/lib/esm/steps/013_printNextSteps.js.map +1 -1
  6. package/package.json +2 -2
  7. package/template/apps/qpqjs/bootstrap.qpq.ts +6 -0
  8. package/template/docusaurus/docs/actions/core/ai/ask-ai-prompt-stream.md +21 -1
  9. package/template/docusaurus/docs/actions/core/ai/ask-ai-prompt.md +9 -3
  10. package/template/docusaurus/docs/actions/core/array/ask-flat-map-parallel-batch.md +1 -1
  11. package/template/docusaurus/docs/actions/core/array/ask-map-parallel-batch.md +2 -2
  12. package/template/docusaurus/docs/actions/core/date/date-time-math.md +2 -2
  13. package/template/docusaurus/docs/actions/core/event/ask-process-event.md +1 -1
  14. package/template/docusaurus/docs/actions/core/file/ask-file-delete.md +3 -0
  15. package/template/docusaurus/docs/actions/core/file/ask-file-exists.md +3 -0
  16. package/template/docusaurus/docs/actions/core/file/ask-file-generate-temporary-secure-url.md +3 -0
  17. package/template/docusaurus/docs/actions/core/file/ask-file-generate-temporary-upload-secure-url.md +3 -0
  18. package/template/docusaurus/docs/actions/core/file/ask-file-is-cold-storage.md +3 -0
  19. package/template/docusaurus/docs/actions/core/file/ask-file-list-directory.md +4 -1
  20. package/template/docusaurus/docs/actions/core/file/ask-file-read-binary-contents.md +3 -0
  21. package/template/docusaurus/docs/actions/core/file/ask-file-read-object-json.md +5 -0
  22. package/template/docusaurus/docs/actions/core/file/ask-file-read-text-contents.md +4 -0
  23. package/template/docusaurus/docs/actions/core/file/ask-file-stream-open.md +3 -0
  24. package/template/docusaurus/docs/actions/core/file/ask-file-write-binary-contents.md +3 -0
  25. package/template/docusaurus/docs/actions/core/file/ask-file-write-object-json.md +3 -0
  26. package/template/docusaurus/docs/actions/core/file/ask-file-write-text-contents.md +3 -0
  27. package/template/docusaurus/docs/actions/core/graph-database/ask-graph-database-internal-field-names.md +1 -1
  28. package/template/docusaurus/docs/actions/core/json/ask-decode-json.md +1 -0
  29. package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-delete.md +9 -1
  30. package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-get-all.md +9 -1
  31. package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-get.md +9 -1
  32. package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-query-single.md +3 -0
  33. package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-query.md +4 -1
  34. package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-scan-all.md +2 -0
  35. package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-scan.md +12 -0
  36. package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-update-partial-properties.md +5 -1
  37. package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-update.md +9 -1
  38. package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-upsert-with-retry.md +1 -1
  39. package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-upsert.md +3 -0
  40. package/template/docusaurus/docs/actions/core/network/ask-network-request.md +1 -1
  41. package/template/docusaurus/docs/actions/core/state/ask-reduce-state.md +1 -1
  42. package/template/docusaurus/docs/actions/core/state/ask-state-dispatch.md +1 -1
  43. package/template/docusaurus/docs/actions/core/stream/ask-stream-map.md +1 -1
  44. package/template/docusaurus/docs/actions/core/stream/ask-stream-process.md +2 -1
  45. package/template/docusaurus/docs/actions/core/system/ask-trace-story.md +66 -0
  46. package/template/docusaurus/docs/actions/core/user-directory/ask-user-directory-authenticate-user.md +3 -3
  47. package/template/docusaurus/docs/actions/core/user-directory/ask-user-directory-set-access-token.md +1 -0
  48. package/template/docusaurus/docs/actions/features/event-doc/ask-apply-event-doc-event.md +47 -0
  49. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-append.md +1 -0
  50. package/template/docusaurus/docs/actions/features/event-doc-ai/ask-event-doc-ai-process-send.md +11 -8
  51. package/template/docusaurus/docs/actions/features/web-socket-queue/_category_.json +1 -0
  52. package/template/docusaurus/docs/actions/{webserver/service → features/web-socket-queue}/ask-service-request.md +4 -3
  53. package/template/docusaurus/docs/actions/webserver/generic-data-resource/ask-put-generic-data-resource.md +4 -0
  54. package/template/docusaurus/docs/actions/webserver/generic-data-resource/ask-scan-generic-data-resource.md +4 -0
  55. package/template/docusaurus/docs/actions/webserver/websocket/ask-websocket-send-message.md +2 -2
  56. package/template/docusaurus/docs/architecture-overview.md +2 -0
  57. package/template/docusaurus/docs/config/config-aws/bootstrap-waf.md +36 -0
  58. package/template/docusaurus/docs/config/core/action-processors.md +24 -2
  59. package/template/docusaurus/docs/config/core/ai.md +5 -5
  60. package/template/docusaurus/docs/config/core/bundle-options.md +102 -0
  61. package/template/docusaurus/docs/config/core/global.md +1 -1
  62. package/template/docusaurus/docs/config/core/queue.md +1 -1
  63. package/template/docusaurus/docs/config/features/admin-settings.md +4 -7
  64. package/template/docusaurus/docs/config/features/event-doc-routes.md +2 -0
  65. package/template/docusaurus/docs/config/features/event-doc.md +2 -0
  66. package/template/docusaurus/docs/config/{webserver → features}/state-dispatch-over-websockets.md +2 -2
  67. package/template/docusaurus/docs/config/features/tenant-stores.md +80 -0
  68. package/template/docusaurus/docs/config/features/tenant.md +81 -0
  69. package/template/docusaurus/docs/config/features/tenanted-event-doc.md +51 -0
  70. package/template/docusaurus/docs/config/features/tenanted-web-socket-queue.md +52 -0
  71. package/template/docusaurus/docs/config/{webserver → features}/web-socket-queue.md +11 -8
  72. package/template/docusaurus/docs/config/webserver/websocket.md +3 -3
  73. package/template/package.json +4 -13
  74. package/template/docusaurus/docs/actions/webserver/service/_category_.json +0 -1
@@ -26,6 +26,7 @@ function defineBootstrapWaf(
26
26
  options?: {
27
27
  managedRuleGroups?: WafManagedRuleGroup[];
28
28
  rateLimits?: WafRateLimit[];
29
+ managedRuleOverrides?: Partial<Record<WafManagedRuleGroup, WafManagedRuleOverride[]>>;
29
30
  },
30
31
  ): BootstrapWafQPQConfigSetting;
31
32
  ```
@@ -38,6 +39,7 @@ function defineBootstrapWaf(
38
39
  | --- | --- | --- | --- |
39
40
  | `managedRuleGroups` | `WafManagedRuleGroup[]` | `[common, knownBadInputs, sqli]` | AWS-managed rule groups to enable, each added as a managed-rule-group statement with `overrideAction: none`. See [`WafManagedRuleGroup`](#wafmanagedrulegroup). |
40
41
  | `rateLimits` | `WafRateLimit[]` | `[]` (none) | Rate-based blocking rules (e.g. brute-force throttling on auth endpoints). Evaluated **before** the managed rule groups. See [`WafRateLimit`](#wafratelimit). |
42
+ | `managedRuleOverrides` | `Partial<Record<WafManagedRuleGroup, WafManagedRuleOverride[]>>` | `undefined` (none) | Per-rule action overrides within a managed group, e.g. force a specific rule to `count` instead of the group's default blocking action. See [`WafManagedRuleOverride`](#wafmanagedruleoverride). |
41
43
 
42
44
  ### `WafManagedRuleGroup`
43
45
 
@@ -73,10 +75,29 @@ export interface WafRateLimit {
73
75
  | `limit` | `number` | Maximum requests per 5-minute window per source IP before that IP is blocked (`aggregateKeyType: IP`). |
74
76
  | `uriPathStartsWith` | `string` | Optional. Scope the rate limit to URI paths with this prefix (e.g. `/auth`) via a scope-down byte-match statement. Omit to rate limit all paths. |
75
77
 
78
+ ### `WafManagedRuleOverride`
79
+
80
+ ```typescript
81
+ export interface WafManagedRuleOverride {
82
+ name: string;
83
+ action: WafRuleOverrideAction;
84
+ }
85
+
86
+ export enum WafRuleOverrideAction {
87
+ count = 'count', // Observe and emit metrics for matching requests, but never block them.
88
+ }
89
+ ```
90
+
91
+ | Property | Type | Description |
92
+ | --- | --- | --- |
93
+ | `name` | `string` | Rule name inside the managed group, e.g. `'SizeRestrictions_BODY'`. |
94
+ | `action` | `WafRuleOverrideAction` | Action to force for this rule instead of the group's default. Currently only `count` (observe without blocking) is supported. |
95
+
76
96
  ## Notes
77
97
 
78
98
  - Rate-limit rules are given the lowest priorities (evaluated first as cheap counters), then the managed rule groups follow.
79
99
  - To actually apply these ACLs, a service opts in with `defineWafProtection()` in its (shared) service config; the ACL ARNs are resolved by convention from SSM at deploy time, so the bootstrap must be deployed before the service's API/web stacks.
100
+ - `managedRuleOverrides` is keyed by `WafManagedRuleGroup`; overrides for a group only apply if that group is also present in `managedRuleGroups`.
80
101
 
81
102
  ## Examples
82
103
 
@@ -99,6 +120,21 @@ export default [
99
120
  ];
100
121
  ```
101
122
 
123
+ To let large request bodies through instead of being blocked by the `common` group's default 8kb body-size limit:
124
+
125
+ ```typescript
126
+ import { defineBootstrapWaf, WafManagedRuleGroup, WafRuleOverrideAction } from 'quidproquo-config-aws';
127
+
128
+ export default [
129
+ defineBootstrapWaf({
130
+ managedRuleGroups: [WafManagedRuleGroup.common, WafManagedRuleGroup.knownBadInputs, WafManagedRuleGroup.sqli],
131
+ managedRuleOverrides: {
132
+ [WafManagedRuleGroup.common]: [{ name: 'SizeRestrictions_BODY', action: WafRuleOverrideAction.count }],
133
+ },
134
+ }),
135
+ ];
136
+ ```
137
+
102
138
  ## Related
103
139
 
104
140
  - [defineWafProtection](./waf-protection.md) — a per-service setting that opts a service's API Gateway stage and CloudFront distributions into these web ACLs.
@@ -33,17 +33,39 @@ A reference (usually a relative path string of the form `'/path/to/file::exporte
33
33
 
34
34
  ```typescript
35
35
  // getCustomProcessors.ts
36
- import { ActionProcessorListResolver } from 'quidproquo-core';
36
+ import { ActionProcessorListResolver, actionResult } from 'quidproquo-core';
37
37
 
38
38
  export const getCustomProcessors: ActionProcessorListResolver = async (qpqConfig, dynamicModuleLoader) => ({
39
39
  ['MyDomain.DoThing']: async (payload, session, actionProcessors, logger, updateSession, dynamicModuleLoader, streamRegistry) => {
40
- // execute the action and return its result
40
+ // execute the action, then wrap the outcome with actionResult / actionResultError
41
+ return actionResult({ ok: true });
41
42
  },
42
43
  });
43
44
  ```
44
45
 
45
46
  Each key is an **action type**; each value is the processor that runs when a story yields an action of that type. Because the runtime merges all processor sources into a single map (later sources spread over earlier ones), a source that provides a key matching a built-in action type **overrides** the built-in processor, and a source that provides a new key **registers a custom action**. You can declare `defineActionProcessors` more than once — every declared source is loaded and merged. Each source's `uniqueKey` is derived from its runtime reference.
46
47
 
48
+ ## Returning results from a processor
49
+
50
+ A processor never returns a bare value or throws to the story. It returns an `ActionProcessorResult`, built with helpers from `quidproquo-core`:
51
+
52
+ - `actionResult(value)` wraps a successful result. The story receives `value` from its `yield`.
53
+ - `actionResultError(errorType, errorText, errorStack?)` wraps a failure. `errorType` should come from the action's own error enum. By default the failure ends the story with that `QPQError`; a story that yields the action with `returnErrors` set receives it as an `EitherActionResult` instead.
54
+ - `actionResultErrorFromCaughtError(caughtError, errorMap)` converts a caught exception inside a `try`/`catch`. The map is keyed by the error's runtime `code` (Node fs style: `ENOENT`, `EACCES`) first, then its `name` (AWS SDK style: `NoSuchBucket`, `AccessDenied`); the matching entry builds the `actionResultError`. An unmapped error becomes a `GenericError` whose text names only the unmapped key, never the raw error message, so map every error your processor can realistically hit.
55
+
56
+ ```typescript
57
+ import { actionResult, actionResultError, actionResultErrorFromCaughtError, ErrorTypeEnum } from 'quidproquo-core';
58
+
59
+ try {
60
+ const item = await readTheThing(payload.id);
61
+ return actionResult(item);
62
+ } catch (error) {
63
+ return actionResultErrorFromCaughtError(error, {
64
+ ENOENT: () => actionResultError(ErrorTypeEnum.NotFound, `Thing not found [${payload.id}]`),
65
+ });
66
+ }
67
+ ```
68
+
47
69
  ## Notes
48
70
 
49
71
  - The resolver is invoked with the service's `QPQConfig` and a `dynamicModuleLoader`, so it can build processors that depend on config or lazily load further modules.
@@ -33,7 +33,7 @@ export default [
33
33
  ```typescript
34
34
  function defineAi(
35
35
  aiName: string,
36
- options: QPQConfigAdvancedAiSettings,
36
+ options?: QPQConfigAdvancedAiSettings,
37
37
  ): AiQPQConfigSetting;
38
38
  ```
39
39
 
@@ -43,7 +43,7 @@ function defineAi(
43
43
 
44
44
  The name of the AI config, and its `uniqueKey` within the config. This is the value you pass as the `aiName` option to the AI prompt actions to bind these tools to a request.
45
45
 
46
- ### `options` — `QPQConfigAdvancedAiSettings` (required)
46
+ ### `options` — `QPQConfigAdvancedAiSettings` (optional)
47
47
 
48
48
  | Property | Type | Default | Description |
49
49
  | --- | --- | --- | --- |
@@ -56,7 +56,7 @@ The name of the AI config, and its `uniqueKey` within the config. This is the va
56
56
  interface AiToolDefinition {
57
57
  name: string;
58
58
  description: string;
59
- executor: string;
59
+ executor?: string;
60
60
  inputSchema: Record<string, unknown>;
61
61
  }
62
62
  ```
@@ -65,12 +65,12 @@ interface AiToolDefinition {
65
65
  | --- | --- | --- |
66
66
  | `name` | `string` | Tool name the model uses to call it. |
67
67
  | `description` | `string` | Natural-language description of what the tool does — the model reads this to decide when to call it, so make it precise. |
68
- | `executor` | `string` | A `QpqFunctionRuntime` reference (`'/path/to/file::exportedFunction'`) to the story that runs when the model calls the tool. It receives the tool input (validated against `inputSchema`) and returns the tool output. |
68
+ | `executor` | `string` (optional) | A `QpqFunctionRuntime` reference (`'/path/to/file::exportedFunction'`) to the story that runs when the model calls the tool. It receives the tool input (validated against `inputSchema`) and returns the tool output. Omit it to declare a **client-side tool**: the model's call isn't run server-side, the turn halts with the call unresolved, and the caller is expected to resolve it out-of-band (e.g. showing a form) and feed the answer back as the next message. |
69
69
  | `inputSchema` | `Record<string, unknown>` | A JSON Schema describing the tool's input. The model is constrained to produce arguments matching this schema. |
70
70
 
71
71
  ## Notes
72
72
 
73
- - When a bound prompt triggers a tool call, the AI action processor runs the tool's `executor` story and returns its result to the model, looping up to 10 tool-calling steps before finishing.
73
+ - When a bound prompt triggers a tool call for a tool with an `executor`, the AI action processor runs that story and returns its result to the model, looping up to 10 tool-calling steps before finishing. A tool call for a tool with no `executor` is left unresolved for the caller to answer.
74
74
  - Streaming prompts surface each tool call, result, and approval request as `ToolCall` / `ToolResult` / `ToolApprovalRequest` parts on the stream — see [askAiPromptStream](../../actions/core/ai/ask-ai-prompt-stream.md#aistreamparttype).
75
75
 
76
76
  ## Related
@@ -0,0 +1,102 @@
1
+ ---
2
+ title: defineBackendBundleOptions / defineFrontendBundleOptions
3
+ description: Tune how the service's backend and frontend bundles are built: externals, ignored modules, suppressed warnings, and shared frontend singletons.
4
+ ---
5
+
6
+ # defineBackendBundleOptions / defineFrontendBundleOptions
7
+
8
+ Declares **bundler options** for the service. These settings deploy nothing themselves: they are read by the build layer (`quidproquo-deploy-webpack` / `quidproquo-deploy-rspack`) when it bundles the service's backend code and frontend views. You can declare either setting more than once; the build layer merges every declared block (later lists concatenate onto earlier ones).
9
+
10
+ All regex-shaped fields are plain regex **source strings**, not `RegExp` objects, because a QPQ config must survive JSON serialization. The bundler compiles them with `new RegExp(...)`.
11
+
12
+ ```typescript
13
+ import { defineBackendBundleOptions, defineFrontendBundleOptions } from 'quidproquo-core';
14
+
15
+ export default [
16
+ defineBackendBundleOptions({
17
+ externals: ['sharp'],
18
+ }),
19
+
20
+ defineFrontendBundleOptions({
21
+ sharedSingletons: ['chakra', 'zod'],
22
+ }),
23
+ ];
24
+ ```
25
+
26
+ ## defineBackendBundleOptions
27
+
28
+ ### Signature
29
+
30
+ ```typescript
31
+ function defineBackendBundleOptions(
32
+ options: BackendBundleOptions,
33
+ ): BackendBundleOptionsQPQConfigSetting;
34
+ ```
35
+
36
+ ### Options: `BackendBundleOptions`
37
+
38
+ | Property | Type | Default | Description |
39
+ | --- | --- | --- | --- |
40
+ | `externals` | `string[]` | `[]` | Packages to `require()` at runtime instead of bundling. Prefer declaring layer-provided packages on the layer itself (`ApiLayer.modules`); this is the escape hatch for non-layer cases. |
41
+ | `ignoreModules` | [`BundleIgnoreModule[]`](#bundleignoremodule) | `[]` | Optional requires inside dependencies that should resolve to nothing (the bundler drops them). |
42
+ | `ignoreWarnings` | [`BundleIgnoreWarning[]`](#bundleignorewarning) | `[]` | Known-noisy build warnings to suppress. |
43
+
44
+ ### `BundleIgnoreModule`
45
+
46
+ | Property | Type | Description |
47
+ | --- | --- | --- |
48
+ | `resource` | `string` | Regex source for the request to drop from the bundle, e.g. `'^original-fs$'`. |
49
+ | `context` | `string` (optional) | Only drop when required from a module matching this regex source, e.g. `'adm-zip'`. |
50
+
51
+ ### `BundleIgnoreWarning`
52
+
53
+ | Property | Type | Description |
54
+ | --- | --- | --- |
55
+ | `module` | `string` (optional) | Regex source matched against the module emitting the warning. |
56
+ | `message` | `string` (optional) | Regex source matched against the warning message. |
57
+
58
+ ## defineFrontendBundleOptions
59
+
60
+ ### Signature
61
+
62
+ ```typescript
63
+ function defineFrontendBundleOptions(
64
+ options: FrontendBundleOptions,
65
+ ): FrontendBundleOptionsQPQConfigSetting;
66
+ ```
67
+
68
+ ### Options: `FrontendBundleOptions`
69
+
70
+ | Property | Type | Default | Description |
71
+ | --- | --- | --- | --- |
72
+ | `sharedSingletons` | `string[]` | `[]` | Substring-matched against the hoisted root dependency names; matches are shared as module-federation **singletons** in the views build. `react`, `react-dom`, react-ish deps, and `quidproquo-web*` are always singletons; this adds app-stack packages (e.g. `'chakra'`, `'zod'`) without baking package names into the bundler. |
73
+
74
+ ## Notes
75
+
76
+ - Multiple declarations merge: `qpqCoreUtils.getBackendBundleOptions` / `getFrontendBundleOptions` concatenate the lists from every declared setting, so a shared config block and a service-specific block can each contribute options.
77
+ - Backend externals combine with the modules declared on the service's Lambda layers; the layer is the preferred home for anything a layer actually provides.
78
+
79
+ ## Examples
80
+
81
+ ```typescript
82
+ import { defineBackendBundleOptions } from 'quidproquo-core';
83
+
84
+ export default [
85
+ defineBackendBundleOptions({
86
+ // Native module provided outside the bundle
87
+ externals: ['sharp'],
88
+
89
+ // adm-zip optionally requires original-fs (an Electron shim) - drop it
90
+ ignoreModules: [{ resource: '^original-fs$', context: 'adm-zip' }],
91
+
92
+ // liquidjs emits a known-noisy parse warning
93
+ ignoreWarnings: [{ module: 'liquidjs', message: 'module\\.createRequire failed parsing argument' }],
94
+ }),
95
+ ];
96
+ ```
97
+
98
+ ## Related
99
+
100
+ - [defineFederatedModuleStore](./federated-module-store.md): load story code from a store at runtime instead of only the bundled zip.
101
+ - [defineApiBuildPath](./api-build-path.md): where the bundled backend output lives.
102
+ - **Build implementation:** `getWebpackConfigForQpq` / `getRspackConfigForQpq` and `getQpqBundleExternals` in `quidproquo-deploy-webpack` / `quidproquo-deploy-rspack`.
@@ -40,4 +40,4 @@ function defineGlobal<T>(
40
40
  - [defineParameter](./parameter.md) — for values that change at runtime.
41
41
  - [defineSecret](./secret.md) — for sensitive values.
42
42
  - [defineApplicationVersion](./application-version.md) — a specialised global that records the service's version string.
43
- - [defineWebSocketQueue](../webserver/web-socket-queue.md) — stores its event-bus and user-directory names in globals like these.
43
+ - [defineWebSocketQueue](../features/web-socket-queue.md) — stores its event-bus and user-directory names in globals like these.
@@ -56,7 +56,7 @@ Each key is matched against a delivered message's `type` field. The key may be a
56
56
  | `concurrency` | `number` | `1` | Consumer concurrency hint. |
57
57
  | `maxTries` | `number` | `1` | How many times a message is delivered before it is sent to the dead-letter queue (SQS `maxReceiveCount`). |
58
58
  | `ttRetryInSeconds` | `number` | `900` | Retry/visibility timeout in seconds — how long a message stays invisible while being processed before it can be redelivered. Also used as the consumer Lambda timeout. Capped at 900 (15 minutes). |
59
- | `hasDeadLetterQueue` | `boolean` | `true` | Whether a dead-letter queue backs the main queue. |
59
+ | `hasDeadLetterQueue` | `boolean` | `true` | Whether a dead-letter queue backs the main queue. Note: the AWS deploy currently always provisions a DLQ; this flag is recorded in the config but not yet honoured by `QpqCoreQueueConstruct`. |
60
60
  | `eventBusSubscriptions` | `string[]` | `[]` | Names of [event buses](./event-bus.md) this queue subscribes to. Each subscribed bus's messages are delivered into this queue. |
61
61
  | `maxConcurrentExecutions` | `number` | – | Reserved concurrent executions for the consumer Lambda (caps parallelism). |
62
62
  | `isFifo` | `boolean` | `false` | Creates a **FIFO** queue that preserves per-group ordering and supports deduplication. See [FIFO queues](#fifo-queues). |
@@ -53,7 +53,6 @@ The root domain the admin service is hosted on. Used to stand up the admin WebSo
53
53
  export interface QPQConfigAdvancedLogSettings extends QPQConfigAdvancedSettings {
54
54
  logRetentionDays?: number;
55
55
  coldStorageAfterDays?: number;
56
- claudeAiApiKeySecretName?: string;
57
56
  services?: string[];
58
57
  }
59
58
  ```
@@ -62,7 +61,6 @@ export interface QPQConfigAdvancedLogSettings extends QPQConfigAdvancedSettings
62
61
  | --- | --- | --- | --- |
63
62
  | `logRetentionDays` | `number` | – (no expiry) | How many days to keep the raw log objects on the logs storage drive before they are deleted. When unset, logs never expire. If `coldStorageAfterDays` is set, the effective retention is padded so that it is at least `coldStorageAfterDays + 180` days — logs always outlive the cold-storage transition window. |
64
63
  | `coldStorageAfterDays` | `number` | – (no transition) | When greater than 0, adds a lifecycle rule transitioning log objects to the `DEEP_COLD_STORAGE` tier after this many days, to cut storage cost for old logs. |
65
- | `claudeAiApiKeySecretName` | `string` | `''` | Name of the secret holding a Claude AI API key, exposed to the admin service as the `claudeAi-api-key` global. Enables the admin dashboard's log-chat feature (asking questions about a log with Claude). |
66
64
  | `services` | `string[]` | `[]` | The list of service names the admin dashboard should surface, exposed as the `qpq-serviceNames` global. Returned by the `/admin/services` route. |
67
65
  | `deprecated` | `boolean` | `false` | Inherited from `QPQConfigAdvancedSettings`; marks the log storage drives and key-value stores as deprecated. |
68
66
 
@@ -73,10 +71,11 @@ export interface QPQConfigAdvancedLogSettings extends QPQConfigAdvancedSettings
73
71
  - **Log storage** — a `QPQ_LOGS_STORAGE_DRIVE_NAME` storage drive (with an `onCreate` handler and the retention / cold-storage lifecycle rules), plus a `QPQ_LOG_REPORTS_STORAGE_DRIVE_NAME` drive (30-day retention) for generated reports.
74
72
  - **Log indexes** — key-value stores for correlations (indexed by `runtimeType` and `fromCorrelation`, both sorted by `startedAt`, with a `ttl` attribute), log messages, and a log list.
75
73
  - **Auth routes** — `POST /login`, `POST /refreshToken`, `POST /challenge`, authenticated against the [admin user directory](./admin-user-directory.md).
76
- - **Log routes** — `GET /admin/services`, `POST /log/list`, `GET /log/{correlationId}`, its `/toggle`, `/children`, `/hierarchies`, `/downloadurl`, the log-log list, and the `/log/chat` message endpoints — all (except the service list) requiring an admin token.
74
+ - **Log routes** — `GET /admin/services`, `POST /log/list`, `GET /log/{correlationId}`, its `/toggle`, `/children`, `/hierarchies`, `/downloadurl`, and the log-log list — all (except the service list) requiring an admin token.
75
+ - **Log chat** — a `defineEventDocAi` instance scoped to `docId` = log correlation id, with two tools (`getLogActions`, `getLogAction`) instead of the log's JSON pasted into the prompt. Runs on `AiModel.ClaudeSonnet46` over the same admin WebSocket connection, not a separate HTTP route.
77
76
  - **WebSocket sync** — an admin event bus (`qpq-admin-wsq`), a `defineWebSocketQueue`, and queues that fan out client messages (config sync, mark-log-checked, refresh-metadata) using `WebsocketAdminClientMessageEventType`.
78
77
  - **Alarms** — a `defineNotifyError` publishing to an `admin-notifier` event bus, with a queue handling its Error / Timeout / Throttle events.
79
- - **Globals** — `qpq-serviceNames`, `qpq-log-retention-days`, and `claudeAi-api-key`.
78
+ - **Globals** — `qpq-serviceNames` and `qpq-log-retention-days`.
80
79
  - **Audit sessions** — spreads in [defineAdminSessionEventDoc](./admin-session-event-doc.md) for the admin UI's per-login session document.
81
80
 
82
81
  ## Examples
@@ -87,12 +86,10 @@ import { defineAdminSettings, defineAdminUserDirectory } from 'quidproquo-featur
87
86
  export default [
88
87
  ...defineAdminUserDirectory({ owner: { module: 'log' } }),
89
88
 
90
- // Retain logs 30 days, move to deep cold storage after 7,
91
- // enable the Claude log-chat feature.
89
+ // Retain logs 30 days, move to deep cold storage after 7.
92
90
  ...defineAdminSettings('log', 'example.com', {
93
91
  logRetentionDays: 30,
94
92
  coldStorageAfterDays: 7,
95
- claudeAiApiKeySecretName: 'claude-api-key',
96
93
  services: ['api', 'workers'],
97
94
  }),
98
95
  ];
@@ -60,6 +60,8 @@ The single `options` argument is an `EventDocRoutesOptions`:
60
60
  | `version` | `number` | `1` | Version number for the `/v{version}` path prefix on every route. |
61
61
  | `eventValidator` | `string` | – | Name of a registered inline function (see `defineInlineFunction`). When set, every append invokes it with `{ event, events }` to reject lifecycle- or payload-invalid events before they reach the log. The frontend editor runs the same rule for instant feedback. |
62
62
  | `eventRenderer` | `string` | – | Name of a registered inline function. When set, a `GET {basePath}/{id}/render` route is mounted; it invokes the renderer with the document's full `{ events }` log, which folds + renders to HTML. |
63
+ | `onPublish` | `string` | – | Name of a registered inline function. When set, every successful append of a Publish event invokes it with `{ docId, event, summary }`, after the event is durably written and the summary re-derived. This is the seam for syncing a folded document into a materialized read model. Errors propagate to the caller: the event has landed but the side effect did not, so the caller learns the read model may be stale. |
64
+ | `scopeResolver` | `string` | – | Name of a registered inline function. When set, every route invokes it with `{ event }` before running; a non-null result becomes the ambient storage scope for the whole request, transparently partitioning the collection's stores and assets (e.g. per-tenant). Null means unscoped. Omit for collections that never partition. |
63
65
 
64
66
  ### `RouteAuthSettings`
65
67
 
@@ -47,6 +47,8 @@ function defineEventDoc(options: EventDocRoutesOptions): QPQConfig;
47
47
  | `version` | `number` | no | Route version prefix (`/v{version}`), default `1`. |
48
48
  | `eventValidator` | `string` | no | Inline-function name run on every append to reject invalid events. |
49
49
  | `eventRenderer` | `string` | no | Inline-function name that folds + renders the log to HTML; mounting it adds a `GET {basePath}/{id}/render` route. |
50
+ | `onPublish` | `string` | no | Inline-function name invoked with `{ docId, event, summary }` after every successful Publish append: the seam for syncing the folded document into a materialized read model. |
51
+ | `scopeResolver` | `string` | no | Inline-function name every route invokes with `{ event }` to resolve the request's ambient storage scope (e.g. per-tenant); null means unscoped. |
50
52
 
51
53
  ## Examples
52
54
 
@@ -10,7 +10,7 @@ Registers an action processor override that redirects the core **State** domain'
10
10
  - **Built from:** `defineActionProcessors(...)` — it overrides the `StateActionType.Dispatch` processor with the WebSocket queue's `getStateDispatch` implementation. That implementation reads the current connection info and sends a typed `StateDispatch` server message to the client over the [WebSocket queue](./web-socket-queue.md). It takes no arguments.
11
11
 
12
12
  ```typescript
13
- import { defineStateDispatchOverWebsockets } from 'quidproquo-webserver';
13
+ import { defineStateDispatchOverWebsockets } from 'quidproquo-features';
14
14
 
15
15
  export default [
16
16
  defineStateDispatchOverWebsockets(),
@@ -38,5 +38,5 @@ Because it operates on the connection context the [WebSocket queue](./web-socket
38
38
  ## Related
39
39
 
40
40
  - [defineWebSocketQueue](./web-socket-queue.md) — provides the connection context and outbound messaging this override relies on.
41
- - [defineWebsocket](./websocket.md) — the raw WebSocket API underneath the queue.
41
+ - [defineWebsocket](../webserver/websocket.md) — the raw WebSocket API underneath the queue.
42
42
  - [askStateDispatch](../../actions/core/state/ask-state-dispatch.md) and [askReduceState](../../actions/core/state/ask-reduce-state.md) — the core State-domain effects whose dispatch is redirected to the client.
@@ -0,0 +1,80 @@
1
+ ---
2
+ title: defineTenantStores
3
+ description: Declare the data stores for the tenant feature, the tenant event-doc collection and the materialized record table.
4
+ ---
5
+
6
+ # defineTenantStores
7
+
8
+ Declares the **owner-only stores** that back the tenant (org) feature, without any routes or inline functions. It returns a `QPQConfig` (an array of config settings) that expands to:
9
+
10
+ 1. The **tenant event-doc collection**: a [defineEventDocSummary](./event-doc-summary.md) call for the `tenants` store (summary table, append-only event log, and asset drive). This is the audit-trailed source of truth for tenant state.
11
+ 2. The **materialized tenant record store**: a [key-value store](../core/key-value-store.md) named `tenantRecords` (partition key `tenantId`). A fast-read table synced from the event doc on publish; it is never written directly by request handlers.
12
+
13
+ The **user-tenant membership links store** (`userTenantLinks`, partition key `userId`) is declared separately, directly by [defineTenant](./tenant.md) — every service refs it via the same `owner`, so it isn't part of this helper.
14
+
15
+ - **On AWS:** deploys everything [defineEventDocSummary](./event-doc-summary.md) deploys (two DynamoDB tables plus an S3 bucket), plus one more DynamoDB table (via [defineKeyValueStore](../core/key-value-store.md)) for the record store.
16
+
17
+ ```typescript
18
+ import { defineTenantStores } from 'quidproquo-features';
19
+
20
+ export default [
21
+ ...defineTenantStores(),
22
+ ];
23
+ ```
24
+
25
+ You rarely call this directly: [defineTenant](./tenant.md) composes it along with the routes and inline functions, gated to the owner's deploy. Call it on its own only when you want the stores without the tenant routes.
26
+
27
+ ## Signature
28
+
29
+ ```typescript
30
+ function defineTenantStores(): QPQConfig;
31
+ ```
32
+
33
+ ## Parameters
34
+
35
+ None. All store names are fixed constants exported from `quidproquo-features`:
36
+
37
+ | Constant | Value | Store |
38
+ | --- | --- | --- |
39
+ | `TENANT_EVENTDOC_STORE` | `'tenants'` | The tenant event-doc collection. |
40
+ | `TENANT_RECORD_STORE` | `'tenantRecords'` | The materialized tenant record table. |
41
+
42
+ The membership table's constant, `USER_TENANT_LINKS_STORE` (`'userTenantLinks'`), is also exported, but the store itself is declared by [defineTenant](./tenant.md), not here.
43
+
44
+ ## Store row shapes
45
+
46
+ The record store holds `TenantRecord` rows, derived from the tenant event doc on publish:
47
+
48
+ ```typescript
49
+ type TenantRecord = {
50
+ tenantId: string;
51
+ name: string;
52
+ brandColors?: Record<string, string>;
53
+ logoUrl?: string;
54
+ createdAt: QpqIsoDateTime;
55
+ updatedAt: QpqIsoDateTime;
56
+ createdByUserId: string;
57
+ status: TenantStatus;
58
+ };
59
+ ```
60
+
61
+ The membership store holds `UserTenantLinks` rows:
62
+
63
+ ```typescript
64
+ type UserTenantLinks = {
65
+ userId: string;
66
+ tenantIds: string[];
67
+ };
68
+ ```
69
+
70
+ ## Notes
71
+
72
+ - The tenant event-doc collection is the source of truth; the `tenantRecords` table is a read model. The sync between them is the `askTenantOnPublish` inline function, which [defineTenant](./tenant.md) registers and wires into the collection's `onPublish` hook.
73
+ - Creating a tenant appends its id to the caller's `UserTenantLinks` row, so the creator becomes the tenant's first member.
74
+ - Services that do **not** own these stores still call [defineTenant](./tenant.md) (with the same `owner`) to get the scope resolver and a cross-module reference to the membership table — they never call `defineTenantStores` themselves.
75
+
76
+ ## Related
77
+
78
+ - [defineTenant](./tenant.md): composes these stores with the routes and inline functions; the usual entry point.
79
+ - [defineEventDocSummary](./event-doc-summary.md): the event-doc store helper this composes for the `tenants` collection.
80
+ - [defineKeyValueStore](../core/key-value-store.md): the core setting behind the record table.
@@ -0,0 +1,81 @@
1
+ ---
2
+ title: defineTenant
3
+ description: Wire up org/tenant support across every service in one call — the registry (stores, publish sync, routes) on the owner, the scope resolver everywhere.
4
+ ---
5
+
6
+ # defineTenant
7
+
8
+ Wires up **everything for org/tenant support**, declared identically in every service that needs it. You always pass the same `owner`; what actually materializes depends on whether the current module is that owner. It returns a `QPQConfig` (an array of config settings) composed of:
9
+
10
+ - The scope-resolver and connection-scope-resolver [inline functions](../core/inline-function.md) (`TENANT_SCOPE_RESOLVER_FN`, `TENANT_CONNECTION_SCOPE_RESOLVER_FN`) — registered in **every** service, so each can tenant-scope its own collections and WebSocket connections. The tenant collection itself is an ordinary tenanted collection too: a tenant doc lives in whatever scope the request that created it ran under (the creator's personal partition, or the active tenant when an org creates a sub-tenant), and every tenant route resolves its scope with the same resolver. The cross-scope registry surface is the membership table plus the materialized `TenantRecord` store below, both unscoped.
11
+ - The `userTenantLinks` membership table ([key-value store](../core/key-value-store.md)) — declared with `owner` everywhere, so non-owner services get a cross-module reference to the owner's table instead of their own copy.
12
+ - Everything else, gated to the owner's deploy only via [defineServiceSettings](../core/service-settings.md):
13
+ - The tenant stores ([defineTenantStores](./tenant-stores.md)): the tenant event-doc collection plus the materialized record table.
14
+ - The publish-to-record-store sync: an inline function (`askTenantOnPublish`) that runs on every published tenant document, re-folds the full event log, and upserts the resulting `TenantRecord`. It is a plain upsert of the fold result, so publish retries and repair re-runs are safe.
15
+ - The generic event-doc CRUD under `{basePath}/docs` ([defineEventDocRoutes](./event-doc-routes.md) for the `tenants` store, `tenant` type, with the publish sync wired in as `onPublish`).
16
+ - The tenant-specific routes at `{basePath}`: list my tenants, create, get record, and get logo.
17
+
18
+ - **On AWS:** on the owner's deploy, this deploys everything [defineTenantStores](./tenant-stores.md) deploys (two DynamoDB tables and an S3 bucket) plus the API Gateway routes and Lambda handlers from [defineEventDocRoutes](./event-doc-routes.md) and the four tenant routes below. On every other service's deploy, only the `userTenantLinks` reference resolves (no new table); the inline functions deploy no infrastructure of their own anywhere.
19
+
20
+ ```typescript
21
+ import { defineTenant } from 'quidproquo-features';
22
+
23
+ // Declare identically in every service — the owner service and any other
24
+ // service that needs to tenant-scope its own collections.
25
+ export default [
26
+ ...defineTenant({
27
+ owner: { module: 'ca' },
28
+ basePath: '/tenants',
29
+ routeAuthSettings: { userDirectoryName: 'users' },
30
+ }),
31
+ ];
32
+ ```
33
+
34
+ ## Routes mounted
35
+
36
+ All paths are prefixed with the version segment `/v{version}` (default `/v1`) and use the `routeAuthSettings` you pass:
37
+
38
+ | Method | Path | Purpose |
39
+ | --- | --- | --- |
40
+ | `GET` | `{basePath}` | The authenticated user's tenants, as `EventDocSummary` rows. Runs under the request's scope: memberships homed in the caller's current partition hydrate live (drafts included); the rest hydrate from the published `TenantRecord` registry. |
41
+ | `POST` | `{basePath}` | Create a tenant (body `{ name }`); the caller becomes its first member. Runs under the request's scope, so the new tenant doc lands in the caller's current partition. |
42
+ | `GET` | `{basePath}/{id}` | One tenant's materialized record (the fast path). Members only: non-members get `Forbidden`, a missing record gets `NotFound`. |
43
+ | `GET` | `{basePath}/{id}/logo` | A presigned, short-lived URL for the tenant's logo blob. Members only: non-members get `Forbidden`; a missing record or a tenant with no logo gets `NotFound`. Presigned in the scope the tenant doc was published under (recorded on the `TenantRecord`), not the reader's own scope, since the logo asset lives in the doc's home partition. |
44
+
45
+ On top of these, the full generic event-doc route set (create, append, list, assets, and so on) is mounted under `{basePath}/docs`; see [defineEventDocRoutes](./event-doc-routes.md#routes-mounted) for the list.
46
+
47
+ ## Signature
48
+
49
+ ```typescript
50
+ function defineTenant(options: TenantOptions): QPQConfig;
51
+ ```
52
+
53
+ ## Parameters
54
+
55
+ The single `options` argument is a `TenantOptions` (a `TenantRoutesOptions` plus `owner`):
56
+
57
+ | Property | Type | Default | Description |
58
+ | --- | --- | --- | --- |
59
+ | `owner` | `CrossModuleOwner & { module: string }` | – (required) | The service that owns the tenant registry, e.g. `{ module: 'ca' }`. Pass the **same** value in every service's `defineTenant` call; the registry (stores, publish sync, management routes) only materializes when the deploying module matches this. |
60
+ | `basePath` | `` `/${string}` `` | – (required) | URL prefix the tenant routes mount under, e.g. `/tenants`. The generic event-doc CRUD mounts under `{basePath}/docs`. Only used on the owner's deploy, but required on every call for type consistency across services. |
61
+ | `routeAuthSettings` | `RouteAuthSettings` | – (required) | Auth applied to every mounted route (see [route](../webserver/route.md)). Required here, unlike the generic event-doc routes: tenant routes are meaningless unauthenticated, since membership keys off the user. Only used on the owner's deploy. |
62
+ | `version` | `number` | `1` | Version number for the `/v{version}` path prefix on every route. Only used on the owner's deploy. |
63
+ | `tenantHeaderName` | `string` | `'x-qpq-tenant-id'` | The header the client sends its selected tenant id on. Exposed to the tenant routes as the `tenantHeaderName` global, which the scope resolver reads. |
64
+
65
+ ## Notes
66
+
67
+ - Tenant state is event-sourced: the `tenants` event-doc collection is the audit-trailed source of truth, and the `tenantRecords` table is a read model synced on publish. Request handlers never write the record table directly.
68
+ - The `tenants` collection is scope-resolved like any other tenanted collection (see [defineTenantedEventDoc](./tenanted-event-doc.md)): a doc is only visible/editable from the scope that owns it, including through the generic CRUD under `{basePath}/docs`. There is no cross-scope doc read — listing memberships homed in another scope goes through the published `TenantRecord`, not the doc store.
69
+ - The `TenantRecord` produced by the publish sync carries `tenantId`, `name`, `brandColors`, `logo` (an asset ref, resolved to a URL via the get-logo route above), `scope` (the storage scope the doc was published under, used to presign the logo for cross-scope readers), `createdAt`, `updatedAt`, `createdByUserId`, and a `status` derived from the summary (`deleted` when the summary has a `deletedAt`, otherwise `active`).
70
+ - `defineTenant` registers the scope resolver but does not apply it to anything. To tenant-scope one of your own collections, pass `TENANT_SCOPE_RESOLVER_FN` as that collection's `scopeResolver` option (or use [defineTenantedEventDoc](./tenanted-event-doc.md), which does this for you).
71
+ - The scope resolver always resolves to a typed scope — a membership-checked `TENANT#<id>` for a request that names a tenant, or the caller's own `PERSONAL#<userId>` when it doesn't. A tenant-scoped collection or connection is never left unscoped.
72
+ - Every service — owner and non-owner alike — calls `defineTenant` with the same `owner`. There is no separate call for non-owning services anymore: the gating happens internally via [defineServiceSettings](../core/service-settings.md).
73
+
74
+ ## Related
75
+
76
+ - [defineTenantStores](./tenant-stores.md): the store half of this helper (owner-only).
77
+ - [defineTenantedEventDoc](./tenanted-event-doc.md): a `defineEventDoc` with `TENANT_SCOPE_RESOLVER_FN` pre-wired as `scopeResolver`.
78
+ - [defineEventDocRoutes](./event-doc-routes.md): the generic CRUD mounted under `{basePath}/docs`, and home of the `scopeResolver` / `onPublish` options.
79
+ - [defineWebSocketQueue](./web-socket-queue.md): where the tenant connection-scope resolver plugs in.
80
+ - [defineTenantedWebSocketQueue](./tenanted-web-socket-queue.md): a `defineWebSocketQueue` with that resolver pre-wired.
81
+ - [defineServiceSettings](../core/service-settings.md): the per-module gating mechanism this uses to materialize the registry only on the owner.
@@ -0,0 +1,51 @@
1
+ ---
2
+ title: defineTenantedEventDoc
3
+ description: A defineEventDoc with the tenant scope resolver pre-wired, so the collection's stores and assets partition per tenant.
4
+ ---
5
+
6
+ # defineTenantedEventDoc
7
+
8
+ A [defineEventDoc](./event-doc.md) with the tenant scope resolver pre-wired as its `scopeResolver`, so the collection's stores and assets partition per tenant: request header → membership check → tenant scope, or the caller's own personal scope when no header is sent — the collection is never unscoped. It takes the same arguments as `defineEventDoc` minus `scopeResolver`, which it fills in for you.
9
+
10
+ The deploying service must still register the resolver implementation by calling [defineTenant](./tenant.md) (with the same `owner` used everywhere else). Use plain [defineEventDoc](./event-doc.md) for collections that never partition by tenant.
11
+
12
+ ```typescript
13
+ import { defineTenantedEventDoc, defineTenant } from 'quidproquo-features';
14
+
15
+ export default [
16
+ ...defineTenant({
17
+ owner: { module: 'ca' },
18
+ basePath: '/tenants',
19
+ routeAuthSettings: { userDirectoryName: 'users' },
20
+ }),
21
+
22
+ ...defineTenantedEventDoc({
23
+ storeName: 'content',
24
+ type: 'article',
25
+ basePath: '/articles',
26
+ routeAuthSettings: { userDirectoryName: 'users' },
27
+ }),
28
+ ];
29
+ ```
30
+
31
+ ## Signature
32
+
33
+ ```typescript
34
+ function defineTenantedEventDoc(options: TenantedEventDocOptions): QPQConfig;
35
+ ```
36
+
37
+ `TenantedEventDocOptions` is `EventDocRoutesOptions` with `scopeResolver` omitted — see [defineEventDoc](./event-doc.md#parameters) for the remaining options.
38
+
39
+ ## Parameters
40
+
41
+ Same as [defineEventDoc](./event-doc.md#parameters): `storeName`, `type`, `basePath`, `routeAuthSettings`, `version`, `eventValidator`, `eventRenderer`, `onPublish` (without `scopeResolver`, which this always sets to `TENANT_SCOPE_RESOLVER_FN`).
42
+
43
+ ## Returns
44
+
45
+ Same as [defineEventDoc](./event-doc.md) — a `QPQConfig` array with the collection's `scopeResolver` already pointed at the tenant scope resolver.
46
+
47
+ ## Related
48
+
49
+ - [defineEventDoc](./event-doc.md) — the underlying define this pre-configures.
50
+ - [defineTenant](./tenant.md) — registers `TENANT_SCOPE_RESOLVER_FN`, the inline function this wires in by name.
51
+ - [defineTenantedWebSocketQueue](./tenanted-web-socket-queue.md) — the same pattern applied to a WebSocket queue's `connectionScopeResolver`.
@@ -0,0 +1,52 @@
1
+ ---
2
+ title: defineTenantedWebSocketQueue
3
+ description: A defineWebSocketQueue with the tenant scope resolution pre-wired as the connection scope resolver.
4
+ ---
5
+
6
+ # defineTenantedWebSocketQueue
7
+
8
+ A [defineWebSocketQueue](./web-socket-queue.md) with tenant scope resolution pre-wired as its `connectionScopeResolver`: a tenant id claimed in the WebSocket Authenticate handshake is membership-checked before it is stored, and a handshake with no claim stores the user's own personal scope instead of leaving the connection unscoped. It takes the same arguments as `defineWebSocketQueue` minus `connectionScopeResolver`, which it fills in for you.
9
+
10
+ The deploying service must still register the resolver implementation by calling [defineTenant](./tenant.md) (with the same `owner` used everywhere else).
11
+
12
+ ```typescript
13
+ import { defineTenantedWebSocketQueue, defineTenant } from 'quidproquo-features';
14
+
15
+ export default [
16
+ ...defineTenant({
17
+ owner: { module: 'ca' },
18
+ basePath: '/tenants',
19
+ routeAuthSettings: { userDirectoryName: 'users' },
20
+ }),
21
+
22
+ defineTenantedWebSocketQueue('my-event-bus', 'api', 'example.com', {
23
+ userDirectoryName: 'users',
24
+ }),
25
+ ];
26
+ ```
27
+
28
+ ## Signature
29
+
30
+ ```typescript
31
+ function defineTenantedWebSocketQueue(
32
+ eventBusName: string,
33
+ apiName: string,
34
+ rootDomain: string,
35
+ advancedSettings?: QPQConfigAdvancedTenantedWebsocketQueueSettings,
36
+ ): QPQConfig;
37
+ ```
38
+
39
+ `QPQConfigAdvancedTenantedWebsocketQueueSettings` is `QPQConfigAdvancedWebsocketQueueSettings` with `connectionScopeResolver` omitted — see [defineWebSocketQueue](./web-socket-queue.md#advancedsettings--qpqconfigadvancedwebsocketqueuesettings-optional) for the remaining options.
40
+
41
+ ## Parameters
42
+
43
+ Same as [defineWebSocketQueue](./web-socket-queue.md#parameters): `eventBusName`, `apiName`, `rootDomain`, and `advancedSettings` (without `connectionScopeResolver`, which this always sets to `TENANT_CONNECTION_SCOPE_RESOLVER_FN`).
44
+
45
+ ## Returns
46
+
47
+ Same as [defineWebSocketQueue](./web-socket-queue.md#returns) — a `QPQConfig` array with the connection-scope resolver already pointed at the tenant scope resolution.
48
+
49
+ ## Related
50
+
51
+ - [defineWebSocketQueue](./web-socket-queue.md) — the underlying define this pre-configures.
52
+ - [defineTenant](./tenant.md) — registers `TENANT_CONNECTION_SCOPE_RESOLVER_FN` (the inline function this wires in by name) plus the rest of the tenant setup.