@nlabs/reaktor 1.17.0 → 1.18.0

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 (99) hide show
  1. package/README.md +182 -0
  2. package/docs/ASSISTANT.md +138 -0
  3. package/docs/GENERATION.md +15 -0
  4. package/docs/NATIVE_PUSH.md +56 -0
  5. package/lib/actions/alfredFaq.d.ts +80 -0
  6. package/lib/actions/alfredFaq.js +206 -0
  7. package/lib/actions/contentGeneration.d.ts +1 -0
  8. package/lib/actions/contentGeneration.js +25 -6
  9. package/lib/actions/conversations.d.ts +1 -1
  10. package/lib/actions/conversations.js +19 -9
  11. package/lib/actions/crud.js +26 -1
  12. package/lib/actions/index.d.ts +1 -0
  13. package/lib/actions/index.js +2 -1
  14. package/lib/actions/messages.js +23 -7
  15. package/lib/actions/users.js +57 -4
  16. package/lib/assistant/core.d.ts +53 -0
  17. package/lib/assistant/core.js +123 -0
  18. package/lib/assistant/faq.d.ts +26 -0
  19. package/lib/assistant/faq.js +238 -0
  20. package/lib/assistant/faqCore.d.ts +23 -0
  21. package/lib/assistant/faqCore.js +20 -0
  22. package/lib/assistant/index.d.ts +7 -0
  23. package/lib/assistant/index.js +5 -0
  24. package/lib/assistant/service.d.ts +35 -0
  25. package/lib/assistant/service.js +201 -0
  26. package/lib/durableChat/conversations.d.ts +62 -0
  27. package/lib/durableChat/conversations.js +729 -0
  28. package/lib/durableChat/errors.d.ts +6 -0
  29. package/lib/durableChat/errors.js +11 -0
  30. package/lib/durableChat/events.d.ts +22 -0
  31. package/lib/durableChat/events.js +50 -0
  32. package/lib/durableChat/index.d.ts +8 -0
  33. package/lib/durableChat/index.js +10 -0
  34. package/lib/durableChat/membership.d.ts +24 -0
  35. package/lib/durableChat/membership.js +85 -0
  36. package/lib/durableChat/messageTypes.d.ts +76 -0
  37. package/lib/durableChat/messageTypes.js +3 -0
  38. package/lib/durableChat/messages.d.ts +16 -0
  39. package/lib/durableChat/messages.js +257 -0
  40. package/lib/durableChat/transaction.d.ts +10 -0
  41. package/lib/durableChat/transaction.js +53 -0
  42. package/lib/durableChat/types.d.ts +81 -0
  43. package/lib/durableChat/types.js +3 -0
  44. package/lib/index.d.ts +4 -4
  45. package/lib/index.js +5 -3
  46. package/lib/lambdas/actions/websockets.js +4 -4
  47. package/lib/nativePush/graphql.d.ts +14 -0
  48. package/lib/nativePush/graphql.js +105 -0
  49. package/lib/nativePush/index.d.ts +7 -0
  50. package/lib/nativePush/index.js +9 -0
  51. package/lib/nativePush/migrations.d.ts +2 -0
  52. package/lib/nativePush/migrations.js +36 -0
  53. package/lib/nativePush/nativePushContract.fixture.d.ts +22 -0
  54. package/lib/nativePush/nativePushContract.fixture.js +27 -0
  55. package/lib/nativePush/payload.d.ts +2 -0
  56. package/lib/nativePush/payload.js +106 -0
  57. package/lib/nativePush/repository.d.ts +9 -0
  58. package/lib/nativePush/repository.js +120 -0
  59. package/lib/nativePush/service.d.ts +2 -0
  60. package/lib/nativePush/service.js +110 -0
  61. package/lib/nativePush/snsProvider.d.ts +17 -0
  62. package/lib/nativePush/snsProvider.js +165 -0
  63. package/lib/nativePush/types.d.ts +89 -0
  64. package/lib/nativePush/types.js +3 -0
  65. package/lib/organizations/accountLifecycle.d.ts +7 -0
  66. package/lib/organizations/accountLifecycle.js +106 -0
  67. package/lib/organizations/authorization.d.ts +9 -0
  68. package/lib/organizations/authorization.js +40 -0
  69. package/lib/organizations/billing.d.ts +38 -0
  70. package/lib/organizations/billing.js +277 -0
  71. package/lib/organizations/billingProvider.d.ts +4 -0
  72. package/lib/organizations/billingProvider.js +287 -0
  73. package/lib/organizations/billingUpdates.d.ts +9 -0
  74. package/lib/organizations/billingUpdates.js +388 -0
  75. package/lib/organizations/index.d.ts +10 -0
  76. package/lib/organizations/index.js +12 -0
  77. package/lib/organizations/mail.d.ts +10 -0
  78. package/lib/organizations/mail.js +60 -0
  79. package/lib/organizations/memberDirectory.d.ts +15 -0
  80. package/lib/organizations/memberDirectory.js +28 -0
  81. package/lib/organizations/membership.d.ts +19 -0
  82. package/lib/organizations/membership.js +248 -0
  83. package/lib/organizations/passwordRecovery.d.ts +9 -0
  84. package/lib/organizations/passwordRecovery.js +50 -0
  85. package/lib/organizations/plans.d.ts +6 -0
  86. package/lib/organizations/plans.js +40 -0
  87. package/lib/organizations/providerPagination.d.ts +6 -0
  88. package/lib/organizations/providerPagination.js +23 -0
  89. package/lib/organizations/reconciliation.d.ts +11 -0
  90. package/lib/organizations/reconciliation.js +400 -0
  91. package/lib/organizations/signup.d.ts +25 -0
  92. package/lib/organizations/signup.js +169 -0
  93. package/lib/organizations/store.d.ts +25 -0
  94. package/lib/organizations/store.js +167 -0
  95. package/lib/organizations/types.d.ts +138 -0
  96. package/lib/organizations/types.js +3 -0
  97. package/lib/organizations/verification.d.ts +3 -0
  98. package/lib/organizations/verification.js +51 -0
  99. package/package.json +16 -3
package/README.md CHANGED
@@ -1454,6 +1454,166 @@ Reaktor is proprietary commercial software. A valid commercial agreement with
1454
1454
  Nitrogen Labs, Inc. is required to use, copy, modify, or distribute this
1455
1455
  package. See [LICENSE](./LICENSE) for the governing terms.
1456
1456
 
1457
+ ## Durable conversations and messages (opt-in)
1458
+
1459
+ The opt-in durable chat module extends Reaktor with persona-scoped direct/group conversations, invitations, role changes,
1460
+ member removal, ownership transfer, bounded membership epochs, idempotent mutation acknowledgements and ordered
1461
+ ID-only events. Existing conversation/message actions and GraphQL resolvers retain their legacy behavior but
1462
+ exclude durable schema-version-1 records; durable reads and writes require the new policy-aware engines. Community `groups`
1463
+ and posts remain separate from group conversations.
1464
+
1465
+ ### Conversation integration
1466
+
1467
+ The following server-side outline assumes `context`, `verifiedIdentity`, and the three policy callbacks are
1468
+ provided by your application. `verifiedIdentity` has `{userId, personaId}` from verified authentication and current
1469
+ ownership checks; never accept either field as an authority supplied by a client.
1470
+
1471
+ ```ts
1472
+ import {
1473
+ createDurableConversationActions,
1474
+ createDurableConversationRepository
1475
+ } from '@nlabs/reaktor';
1476
+
1477
+ const repository = createDurableConversationRepository(context, verifiedIdentity, {
1478
+ audience: 'web', // Trusted server choice, never copied from an untrusted request.
1479
+ authorizeIdentity, // async (transactionQuery, identity): fresh ownership and application eligibility.
1480
+ classifyName, // (name, contentVersion): application decision metadata.
1481
+ projectName, // (record, audience): authorized current name or undefined.
1482
+ readCollections: ['applicationEligibility'] // Additional collections read by these hooks.
1483
+ });
1484
+ const actions = createDurableConversationActions(verifiedIdentity, repository);
1485
+ const conversation = await actions.getOrCreateDirect({participantPersonaId: 'peer-persona'});
1486
+ ```
1487
+
1488
+ The actions facade also exposes `createGroup`, `inviteMember`, `acceptInvite`, `declineInvite`,
1489
+ `changeMemberRole`, `removeMember`, `leaveConversation`, `transferOwnership`, `listConversations`,
1490
+ `listInvites`, and `getParticipants`. Existing-group mutations take the current `membershipRevision` as
1491
+ `expectedRevision`; accepting or declining an invite uses that invite's `revision` instead. Reuse the original
1492
+ idempotency key and exact payload when retrying a request; changing its payload raises `conflict`.
1493
+
1494
+ Use the actions facade at public boundaries: it validates identifiers, revisions, list bounds, roles and group
1495
+ sizes. The repository handles canonical directs, seven-day invitations, group activation, role/owner transitions
1496
+ and replay receipts. Names are projected only on current reads; mutation receipts contain no name content.
1497
+ Groups initially invite 2–24 peers, become active at three accepted members, and cap active plus pending members
1498
+ at 25. A sole owner may leave and close the group; a nonempty group requires ownership transfer first.
1499
+
1500
+ ### Message integration
1501
+
1502
+ `createDurableMessagePersistence(hooks)` provides validated send/edit/delete operations. Required hooks are
1503
+ `identity`, `run`, `authorizeIdentity`, `authorizeMembership`, `authorizeAttachment`, `authorizeMessageVisible`,
1504
+ `classifyContent` and `appendEvent` (normally `appendChatEvent`). Identity comes from verified server context.
1505
+ The engine owns exact payload replay/conflict checks, sequence allocation, immutable attachment version
1506
+ snapshots, reply references, 15-minute edit eligibility, revision CAS, deletion tombstones, rate limits and
1507
+ atomic event append. It accepts at most 4,000 Unicode characters and four distinct image references. The
1508
+ application owns content classification, current audience projection, original/derivative delivery and any
1509
+ retained-author policy; persistence records must not be returned directly as public message projections.
1510
+ The existing `mobileContentVersion`, `mobileDecision` and `mobilePlatformDisposition` storage names are retained
1511
+ for schema compatibility, with decision values supplied by the caller rather than a built-in platform policy.
1512
+
1513
+ This outline builds on the conversation example. The application-provided `messagePolicy` contains
1514
+ `authorizeIdentity`, `authorizeAttachment`, `authorizeMessageVisible`, and `classifyContent`. Its reads use the
1515
+ transaction query passed into each authorization callback. Replace the example policy/media collection names
1516
+ with every collection your hooks actually read.
1517
+
1518
+ ```ts
1519
+ import {
1520
+ appendChatEvent,
1521
+ createDurableMessagePersistence,
1522
+ createDurableTransaction,
1523
+ createMembershipAuthorizer
1524
+ } from '@nlabs/reaktor';
1525
+
1526
+ const messages = createDurableMessagePersistence({
1527
+ ...messagePolicy,
1528
+ appendEvent: appendChatEvent,
1529
+ authorizeMembership: (read, identity, conversationId, purpose) =>
1530
+ createMembershipAuthorizer(read)(identity, {conversationId, purpose}),
1531
+ identity: verifiedIdentity,
1532
+ run: createDurableTransaction(context, {
1533
+ exclusive: ['chatMessageRequests', 'chatOutbox', 'chatRateLimits', 'conversations', 'messages'],
1534
+ read: ['applicationEligibility', 'hasChatMembership', 'hasConnection', 'images', 'personas', 'users']
1535
+ })
1536
+ });
1537
+
1538
+ const acknowledgement = await messages.sendMessage({
1539
+ attachmentIds: [],
1540
+ clientMessageId: 'client-generated-request-id',
1541
+ content: 'Hello',
1542
+ conversationId: conversation.conversationId,
1543
+ senderPersonaId: verifiedIdentity.personaId
1544
+ });
1545
+ ```
1546
+
1547
+ Persist the client-generated request ID before sending and keep it with the immutable retry payload. A successful
1548
+ send returns only `clientMessageId`, `messageId`, `revision`, and `sequence`. The `editMessage` and `deleteMessage`
1549
+ methods use `messageId`, `conversationId`, a new `idempotencyKey`, and the last acknowledged message revision as
1550
+ `expectedRevision`; editing also takes `content`. They return `messageId`, `revision`, and `sequence`.
1551
+ These are mutation/replay acknowledgements, not delivery or read receipts. Reading history, read receipts,
1552
+ reactions, socket transport, and event delivery remain application integrations. The exported
1553
+ `readDurableMessage` helper only verifies conversation/schema identity; callers still authorize membership,
1554
+ visibility, and the audience-specific projection before returning content.
1555
+
1556
+ ### Current defaults and limits
1557
+
1558
+ These defaults are enforced by the durable chat engines; they are not configurable repository options.
1559
+
1560
+ | Area | Behavior |
1561
+ | --- | --- |
1562
+ | Audience | Conversation name projection defaults to `mobile`; the server may explicitly choose `web`. |
1563
+ | Relationships | Direct creation and group invitations require active personas belonging to different users, mutual active follows, and no block in either direction. Invite acceptance rechecks the inviter relationship. Direct send/socket access rechecks the current relationship. |
1564
+ | Groups | Invite 2–24 distinct peers at creation. The group starts `forming`, activates at three accepted members including the owner, and caps active plus unexpired pending members at 25. |
1565
+ | Names and invitations | Group names must be nonblank and at most 200 UTF-16 code units. Invitations expire after seven days. |
1566
+ | Roles | Owners change roles and transfer ownership. Owners/admins invite; an admin may remove members but not an owner or another admin. An owner must transfer ownership before leaving other active members. |
1567
+ | Lists | Default page size 25; maximum 50. `after` is a document-key cursor, not a timestamp. |
1568
+ | Messages | At most 4,000 Unicode code points and four distinct image references. Sending requires nonblank text or an attachment. |
1569
+ | Edits and deletion | Only the current author may mutate an eligible message. Editing is allowed through 15 minutes after creation; deletion keeps a tombstone and clears text, attachments, and the reply reference. |
1570
+ | Rate limits | New direct/group creation and new invitations share a 50-per-hour user bucket. Sending, editing, and deleting each have a separate 60-per-minute user/persona bucket. |
1571
+ | Identifiers | IDs and idempotency keys use 1–128 ASCII letters, digits, underscores, or hyphens. Revisions are positive safe integers. |
1572
+
1573
+ Applications decide eligibility, moderation/classification, attachment access and content retention. The built-in
1574
+ relationship rules and limits above still apply after those checks; products requiring different rules need an
1575
+ additive shared-library extension. `ChatError` (also exported as `DurableChatError`) exposes a `code` of
1576
+ `invalid_request`, `forbidden`, `conflict`, `rate_limited`, or `unavailable`; map these to your transport without
1577
+ serializing private persistence records or authorization details.
1578
+
1579
+ ### Transactions and schema prerequisites
1580
+
1581
+ `createDurableTransaction(context, collections, query?)` returns an operation runner using Reaktor `useDb`,
1582
+ explicit collections, `allowImplicit: false`, five-second lock timeout and synchronous commit. Every policy hook
1583
+ must use its supplied transaction query for authoritative reads. Never call legacy actions that open their own
1584
+ connection from inside a hook. External provider calls belong outside the transaction.
1585
+
1586
+ `classifyName` is synchronous. `classifyContent` may return a promise, but it runs inside the message transaction;
1587
+ use an already available deterministic policy/decision there and schedule external moderation work separately.
1588
+ `projectName` controls name disclosure on current list reads. The library does not install an approval policy.
1589
+
1590
+ Conversation repositories lock `conversations`, `hasChatMembership`, `hasConversation`, `chatInvites`,
1591
+ `chatRequests`, `chatRateLimits` and `chatOutbox` exclusively; they read `users`, `personas`, `hasConnection`
1592
+ and declared hook collections. Message runners must exclusively lock `messages`, `chatMessageRequests`,
1593
+ `chatRateLimits`, `conversations` and `chatOutbox`, plus any mutation collections used by hooks. Declare all
1594
+ membership, identity, relationship, media and policy read collections. Both engines must share the conversation
1595
+ lock with membership changes so sends and removals have a defined order.
1596
+
1597
+ Supplying an existing query transfers transaction/lock/commit ownership to the caller. It never opens a fallback
1598
+ connection, nests a transaction or commits independently. Commit failures are propagated without automatic
1599
+ retries: clients retry the exact original idempotency key and payload. `appendChatEvent` requires the caller's
1600
+ exclusive conversation/outbox locks and stores only IDs, revisions and sequence metadata.
1601
+
1602
+ Provision document collections above and edge collections `hasChatMembership`, `hasConversation` and
1603
+ `hasConnection` before use. Required unique persistent indexes are:
1604
+
1605
+ - `conversations.directPairKey` (sparse).
1606
+ - `hasChatMembership.currentKey` (sparse), and `(conversationId, personaId, epoch)`.
1607
+ - `chatInvites.currentKey` (sparse).
1608
+ - `messages.(conversationId, senderPersonaId, clientMessageId)` and `(conversationId, sequence)` (sparse).
1609
+ - `chatOutbox.(conversationId, eventSequence)` (sparse).
1610
+
1611
+ Index membership state/role, invite recipient/state/expiry and outbox status/retry/lease fields for bounded reads.
1612
+ Receipt documents use deterministic primary keys. No migration runs automatically. Legacy message histories
1613
+ must be assigned validated sequences and compatible membership grants before enabling durable writes; the
1614
+ message engine rejects conversations containing unmigrated history. Delivery workers, schema migration and
1615
+ transport endpoints remain application integration responsibilities.
1616
+
1457
1617
  ## Generation, sessions, and realtime
1458
1618
 
1459
1619
  See the [complete generation and sessions guide](docs/GENERATION.md) for the provider matrix, server configuration, results, storage, rejection handling, and release policy.
@@ -1520,3 +1680,25 @@ Higgsfield generation includes a typed catalog of 80 verified image/video API va
1520
1680
  ### Grok text and media
1521
1681
 
1522
1682
  Text generation accepts `provider: "grok"`; Reaktor uses server-side `contentGeneration.grokKey` and optional `grokModel` (default `grok-4.7`) for the xAI Responses API. Owned image inputs can accompany a text prompt. Grok image/video models are available through the Higgsfield model catalog and its existing webhook transport; this integration does not add a direct xAI media polling adapter. Application code owns callback persistence, retention, and accounting.
1683
+
1684
+ ## Assistant backend
1685
+
1686
+ Reaktor provides configurable grounded chat, durable support delivery and public FAQ processing. Alfred supplies the UI shell; MetropolisJS's assistant action calls the GraphQL API directly. NitrogenX uses JSON POST requests to `https://api.reaktor.io` (also accepted at `/api`).
1687
+
1688
+ ```graphql
1689
+ mutation Chat($input: JSONObject!) {
1690
+ assistant { chat(input: $input) }
1691
+ }
1692
+ query Faqs($input: JSONObject!) {
1693
+ assistant { faqs(input: $input) }
1694
+ }
1695
+ mutation SubmitSupport($input: JSONObject!) {
1696
+ assistant { submitSupport(input: $input) }
1697
+ }
1698
+ ```
1699
+
1700
+ Each operation takes `input: JSONObject!` and returns JSON at `data.assistant.<operation>`; it does not take a nested result selection set. For chat, send `{input: {context: 'nitrogen', history: [], question: 'What does Nitrogen build?'}}`. Chat returns `answer`, `sources` and `supportSuggested`; FAQs return `items`; support returns `ticketNumber` or `queued: true`.
1701
+
1702
+ Support requires user confirmation, valid contact details and a stable `web-UUID` request ID. Queued acceptance means durable storage for retry, not confirmed email delivery. Read GraphQL `errors` even for HTTP 200 responses. The server selects approved knowledge, origins, database and delivery configuration; caller metadata cannot grant access.
1703
+
1704
+ The backend factories are exported from the server-only `@nlabs/reaktor/assistant` subpath in the release containing the extraction. See [assistant endpoint, configuration and rollout](docs/ASSISTANT.md) for complete requests/responses, validation, limits, adapters and publication/deployment status.
@@ -0,0 +1,138 @@
1
+ # Shared assistant backend
2
+
3
+ The assistant backend lives in Reaktor. Alfred supplies the UI shell; the host app uses the existing MetropolisJS `assistant` action. No changes to MetropolisJS are required.
4
+
5
+ ```text
6
+ Alfred UI → MetropolisJS assistant action → Reaktor GraphQL → shared assistant service
7
+ → Bedrock + approved knowledge
8
+ → CRM / support notification
9
+ → public FAQ actions
10
+ ```
11
+
12
+ The GraphQL operations remain `assistant.chat`, `assistant.faqs` and `assistant.submitSupport`, each accepting `input: JSONObject!` and returning JSON. Public support submissions require explicit confirmation and an opaque `web-UUID` request ID. Keep that ID and the same details when retrying.
13
+
14
+ ## GraphQL endpoint and operations
15
+
16
+ NitrogenX posts JSON `{query, variables}` to `https://api.reaktor.io` (the API also accepts `/api`). This is the GraphQL endpoint; no public `/api/assistant/*` REST endpoint is required. The frontend uses MetropolisJS's `createAction('assistant', flux)` with `app.api.public` set to the root URL.
17
+
18
+ ```graphql
19
+ mutation Chat($input: JSONObject!) {
20
+ assistant { chat(input: $input) }
21
+ }
22
+ query Faqs($input: JSONObject!) {
23
+ assistant { faqs(input: $input) }
24
+ }
25
+ mutation SubmitSupport($input: JSONObject!) {
26
+ assistant { submitSupport(input: $input) }
27
+ }
28
+ ```
29
+
30
+ All operation inputs and results use the JSON scalar `JSONObject!`. Do not add a selection set inside `chat`, `faqs` or `submitSupport`; their result is JSON. Read `data.assistant.chat`, `data.assistant.faqs` or `data.assistant.submitSupport` respectively. Example chat variables:
31
+
32
+ ```json
33
+ {
34
+ "input": {
35
+ "context": "nitrogen",
36
+ "history": [],
37
+ "language": "en-US",
38
+ "name": "Alfred",
39
+ "question": "What does Nitrogen build?"
40
+ }
41
+ }
42
+ ```
43
+
44
+ ```jsonc
45
+ // assistant.chat
46
+ {
47
+ "answer": "An answer grounded in published documentation.",
48
+ "sources": [{"title": "About Nitrogen", "url": "https://nitrogenx.co/about"}],
49
+ "supportSuggested": false
50
+ }
51
+ // assistant.faqs
52
+ {"items": [{"id": "topic-id", "question": "A public question?", "answer": "A grounded answer.", "sources": []}]}
53
+ // assistant.submitSupport (confirmed delivery or durable queue)
54
+ {"ticketNumber": "NX-..."}
55
+ // or
56
+ {"queued": true}
57
+ ```
58
+
59
+ ### Input contract
60
+
61
+ - Chat: `question` is required (1–1,200 trimmed characters). `history` defaults to `[]`, allows six turns with `role: 'user' | 'assistant'` and `text` up to 2,500 characters. Optional `name` and `language` are bounded to 80 and 35 characters. Context must belong to the trusted profile; Nitrogen defaults to `nitrogen`.
62
+ - FAQ: send `{context: 'nitrogen'}` or `{}`. The current Nitrogen profile lists public FAQ answers for the current corpus revision. Context/knowledge metadata do not select a tenant or guarantee product filtering.
63
+ - Support: requires explicit `confirmed: true`, `firstName`, valid `email`, `message` up to 4,000 characters and a stable `requestId: 'web-UUID'`. Optional `company`, `lastName` and `phone` are bounded to 160, 160 and 24 characters. Context selects an approved product label; the server sets the support source.
64
+
65
+ Example confirmed support variables (generate a fresh UUID for a new submission; keep it on retries):
66
+
67
+ ```json
68
+ {"input":{"confirmed":true,"context":"nitrogen","email":"ada@example.com","firstName":"Ada","message":"Please help with my project.","requestId":"web-12345678-1234-1234-1234-123456789012"}}
69
+ ```
70
+
71
+ `ticketNumber` confirms ticket creation and support notification delivery. `queued: true` confirms durable acceptance for retry, not email delivery. An identical retry returns the original receipt; different details under the same ID fail with `payload_conflict`.
72
+
73
+ ### Authorization and failures
74
+
75
+ The managed handler checks an allowlisted browser origin and the API Gateway caller IP before servicing requests. A trusted server profile chooses corpus, database and delivery adapters. Client `knowledge`, `context`, name and language cannot grant permissions or choose storage. Never place provider/API credentials in browser code. Register a new origin/profile before connecting another host.
76
+
77
+ Chat permits 30 requests per caller IP/hour and defaults to 20,000 requests/month for the profile. Support permits three submissions per caller IP/hour and 1,000/month. Visitor IDs are hashed; counters use conditional writes. Knowledge older than 14 days fails closed, and citations must refer to retrieved approved HTTPS sources.
78
+
79
+ GraphQL failures use the standard `errors` array, potentially with HTTP 200. Controlled messages include `assistant_origin_not_allowed`, `assistant_request_too_large`, `invalid_request`, `confirmation_required`, `rate_limited`, `payload_conflict` and `assistant_unavailable`. Do not interpret HTTP success alone as a completed operation. The API bounds serialized input to 24,000 bytes and hides internal provider/storage diagnostics.
80
+
81
+ The server-only `/assistant/faq` maintenance and `/crm/support/tickets/assistant` compatibility routes require Nitrogen's API key; they are not the browser assistant interface. The new GraphQL service calls the FAQ/CRM actions directly. See [MetropolisJS assistant actions](https://github.com/nitrogenlabs/metropolisjs/blob/main/docs/ASSISTANT.md) for client events and state.
82
+
83
+ ## Server configuration
84
+
85
+ Import the backend factories from `@nlabs/reaktor/assistant` once the release containing this subpath is published. They require server AWS credentials; never bundle this module into a frontend.
86
+
87
+ ```ts
88
+ import {createAssistantFaq, createAssistantService} from '@nlabs/reaktor/assistant';
89
+ import {faqAction, submitPhoneSupportTicket} from '@nlabs/reaktor';
90
+
91
+ const database = {databaseName: 'reaktor'};
92
+ const profile = {
93
+ allowedHosts: ['nitrogenx.co', '*.nitrogenx.co', 'reaktor.io'],
94
+ bucket: 'my-assistant-bucket',
95
+ contexts: ['nitrogen', 'reaktor', 'alfred'],
96
+ defaultContext: 'nitrogen',
97
+ organization: 'Nitrogen'
98
+ };
99
+ const assistant = createAssistantService({
100
+ ...profile,
101
+ deliverSupport: (input) => submitPhoneSupportTicket(database, input),
102
+ modelId: 'amazon.nova-micro-v1:0',
103
+ monthlyRequests: 20000
104
+ });
105
+ const faqs = createAssistantFaq({
106
+ ...profile,
107
+ captureEnabled: true,
108
+ request: async <T>(operation: string, input?: unknown): Promise<T> =>
109
+ await faqAction(database, operation, input) as T
110
+ });
111
+ ```
112
+
113
+ NitrogenX's actual server profile is in `apps/api/src/assistant.ts`. It selects the corpus bucket and database on the server, calls the existing Reaktor CRM action directly, and uses the existing FAQ database action directly. The generic factories contain no Nitrogen domains, secret names, API URLs or support inbox. The current `submitPhoneSupportTicket` adapter is Nitrogen-specific; other hosts inject their own delivery adapter. That adapter must return `{notificationStatus: 'sent', ticketNumber}` only after its ticket and notification are confirmed.
114
+
115
+ A frontend `knowledge` object remains application metadata. It cannot choose an S3 bucket, prefix, URL, secret or database. To add another application, register a trusted server profile and map its approved origin to that profile before calling the service. Do not choose profiles from an unauthenticated `appId` or `knowledge` payload.
116
+
117
+ Use a separate bucket or a dedicated `prefix` (for example `customer-a/`) for every tenant. Configure the same bucket/prefix on both factories and enforce matching IAM access. Knowledge is JSON at `[prefix]knowledge/current.json`:
118
+
119
+ ```json
120
+ {
121
+ "fetchedAt": "2026-10-02T12:00:00Z",
122
+ "pages": [{
123
+ "text": "Your published product documentation goes here.",
124
+ "title": "Product documentation",
125
+ "url": "https://nitrogenx.co/docs"
126
+ }]
127
+ }
128
+ ```
129
+
130
+ Retrieval allows only configured HTTPS hosts, bounds source text, and returns citations only from retrieved pages. Knowledge older than 14 days fails closed. Chat history is limited to six turns. The service honors bounded `name` and `language` metadata when constructing answers; these fields never change backend permissions. Nova Micro remains the default chat model and Titan v2 is used for FAQ embeddings.
131
+
132
+ ## Durable jobs and rollout
133
+
134
+ The service writes encrypted pending submissions before reporting queued acceptance. Matching retries return the same receipt; changed payloads with the same ID are rejected. `assistant.retry()` drains support messages. `faqs.processFaqQueue()` groups sanitized public inquiries, quarantines repeated failures and refreshes grounded FAQ answers. Reaktor's scheduled worker runs both independently.
135
+
136
+ Build and test Reaktor before deploying the API and its updated IAM policies. `AssistantBucketName` must name the existing Nitrogen corpus/queue bucket. The template defaults `AssistantWorkerEnabled` to `false` to avoid competing with legacy schedules. After verifying GraphQL chat, FAQ and confirmed support delivery, disable the old Nitrogen assistant delivery and FAQ worker schedules, then set `AssistantWorkerEnabled=true`. Keep the old Lambda versions for rollback. The Nitrogen knowledge crawler and phone IVR remain host integrations; the phone capture may continue writing the shared FAQ queue.
137
+
138
+ This source change does not publish npm packages, deploy AWS resources or disable existing schedules. The legacy Nitrogen Lambda files are retained for rollback and phone compatibility until the shared package release is consumed by those integrations.
@@ -304,3 +304,18 @@ it as `generationConfig.topP`. Values must be finite numbers from 0 through 1,
304
304
  including zero. Omission leaves the provider default untouched. Image requests
305
305
  and unsupported providers reject it before submission. Model-specific sampling
306
306
  policy belongs to callers; Reaktor adds no defaults or application prompts.
307
+
308
+ ## Output whitespace
309
+
310
+ The server-side `generateContent` action accepts `outputTextMode: 'preserve'`
311
+ for Claude Messages and OpenAI/Grok Responses calls. Preserve mode concatenates
312
+ text blocks exactly, adding no separators and keeping surrounding whitespace.
313
+ Claude thinking/redacted-thinking blocks are excluded; unsupported blocks or
314
+ malformed text are rejected in preserve mode. A normally completed, nonblank
315
+ text response is still required.
316
+
317
+ Omit the option or use `'trim'` to keep existing behavior: OpenAI/Grok trim their
318
+ concatenated text; Claude inserts newlines between text blocks before trimming.
319
+ Other providers reject the option before submission. It is local output policy
320
+ and is never sent to a provider; GraphQL input does not expose it. The option does
321
+ not enable or select Claude thinking settings or alter provider request bodies.
@@ -0,0 +1,56 @@
1
+ # Native push through AWS SNS
2
+
3
+ Import `createNativePushService`, `createArangoNativePushRepository`,
4
+ `migrateNativePushRegistry`, and `createNativePushGraphql` from
5
+ `@nlabs/reaktor/native-push`. Existing direct APNs actions are unchanged.
6
+
7
+ ```ts
8
+ const scope = {appId: 'your-app', environment: 'dev'};
9
+ const repository = createArangoNativePushRepository({databaseName: 'your-app-dev'});
10
+ // Explicit rollout operation, never run at import/startup:
11
+ await migrateNativePushRegistry({databaseName: 'your-app-dev'});
12
+ const service = createNativePushService({
13
+ scope, repository, sns: new SNSClient({region: 'us-east-1'}),
14
+ platformApplicationArns: {android: androidArn, iosSandbox: sandboxArn, iosProduction: productionArn},
15
+ isSessionEligible: async (scope, actor) => verifyPersistedLiveSession(scope, actor),
16
+ canDeliver: async (registration, notification) => recheckBusinessAccess(registration.userId, notification)
17
+ });
18
+ const graphql = createNativePushGraphql({service, resolveActor: context => verifiedActor(context)});
19
+ // Mount graphql.mutationFields.nativePush under the app's existing schema.
20
+ ```
21
+
22
+ Actor comes only from verified server context; scope and ARNs come only from
23
+ server configuration. Registration receives installationId/platform/native token
24
+ and (for iOS) sandbox/production APNs environment. Public GraphQL results exclude
25
+ tokens, hashes, endpoint ARNs and session identity. Unregister is actor/revision
26
+ fenced, so delayed old-account cleanup cannot disable a replacement. Session
27
+ revocation remains essential if the client is offline. The registry uses Reaktor
28
+ `useDb`, with explicit unique scope+installation and lookup indexes.
29
+
30
+ Send one stable server-generated notificationId across transports. Version is 1;
31
+ title/body stay generic for private content. Put only non-sensitive navigation
32
+ identifiers in JSON data, never chat text, precise location, tokens or credentials.
33
+ `send` returns per-registration ACCEPTED/SKIPPED/INVALID_ENDPOINT/TRANSIENT_FAILURE/
34
+ PERMANENT_FAILURE. ACCEPTED means provider acceptance, not device display. Native
35
+ and WebSocket clients deduplicate the same envelope by ID. SNS transient retries
36
+ are bounded; invalid registrations are disabled, and authenticated registration
37
+ can reconcile/re-enable endpoints. No durable outbox or delivery guarantees.
38
+
39
+ ## Rollout
40
+
41
+ Each app/environment needs distinct SNS platform applications and bundle/package
42
+ IDs. APNs sandbox versus production must match signed build entitlements. Upload
43
+ APNs credentials and FCM HTTP-v1 service account credentials to AWS/backend secrets,
44
+ not the app repository. FCM is Android transport only; Firebase auth/database is
45
+ not required. Restrict CreatePlatformEndpoint/GetEndpointAttributes/
46
+ SetEndpointAttributes/Publish IAM permissions to configured SNS resources.
47
+
48
+ Apply migrations explicitly; do not trust legacy unauthenticated device rows.
49
+ Rollback by disabling registration/delivery without deleting inbox/registry data.
50
+ Keep reconnect/backfill and recipient authorization in the app. Check physical
51
+ iOS/Android foreground/background/terminated launch, permission denial, token
52
+ rotation, offline logout and account switching before production rollout.
53
+
54
+ See MetropolisJS `docs/NATIVE_NOTIFICATIONS.md` for the optional Expo integration.
55
+ Cloud provisioning, deployment, migrations and npm publication require separate
56
+ approval; unit tests and bundles do not prove live native push.
@@ -0,0 +1,80 @@
1
+ import type { ApiContext } from '../types/auth.types.js';
2
+ interface FaqSource {
3
+ title: string;
4
+ url: string;
5
+ }
6
+ interface Topic {
7
+ _key: string;
8
+ answer?: string;
9
+ answerExpires?: number;
10
+ appId: string;
11
+ embedding?: number[];
12
+ kind: string;
13
+ lastAsked: number;
14
+ product?: string;
15
+ question: string;
16
+ sources?: FaqSource[];
17
+ weight: number;
18
+ }
19
+ interface FaqInput {
20
+ channel: 'website' | 'phone';
21
+ embedding: number[];
22
+ eventId: string;
23
+ product: string;
24
+ question: string;
25
+ revision: number;
26
+ topicId: string;
27
+ }
28
+ export declare const validateFaqInput: (value: unknown) => FaqInput;
29
+ export declare const publicFaq: (topics: Topic[], now: number) => {
30
+ answer: string;
31
+ id: string;
32
+ question: string;
33
+ sources: FaqSource[];
34
+ }[];
35
+ export declare const faqAction: (context: ApiContext, operation: string, value?: unknown) => Promise<{
36
+ items: {
37
+ answer: string;
38
+ id: string;
39
+ question: string;
40
+ sources: FaqSource[];
41
+ }[];
42
+ ok?: undefined;
43
+ candidates?: undefined;
44
+ revision?: undefined;
45
+ duplicate?: undefined;
46
+ topicId?: undefined;
47
+ } | {
48
+ items: Topic[];
49
+ ok?: undefined;
50
+ candidates?: undefined;
51
+ revision?: undefined;
52
+ duplicate?: undefined;
53
+ topicId?: undefined;
54
+ } | {
55
+ items?: undefined;
56
+ ok: boolean;
57
+ candidates?: undefined;
58
+ revision?: undefined;
59
+ duplicate?: undefined;
60
+ topicId?: undefined;
61
+ } | {
62
+ items?: undefined;
63
+ ok?: undefined;
64
+ candidates: {
65
+ id: string;
66
+ question: string;
67
+ score: number;
68
+ }[];
69
+ revision: number;
70
+ duplicate?: undefined;
71
+ topicId?: undefined;
72
+ } | {
73
+ items?: undefined;
74
+ ok?: undefined;
75
+ candidates?: undefined;
76
+ revision?: undefined;
77
+ duplicate: boolean;
78
+ topicId: string;
79
+ }>;
80
+ export {};