@teincfood/core 0.7.9 → 0.8.1

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 (191) hide show
  1. package/README.md +26 -21
  2. package/dist/adapters/auth.d.ts +8 -0
  3. package/dist/adapters/auth.js +36 -0
  4. package/dist/adapters/devices.d.ts +3 -1
  5. package/dist/adapters/devices.js +187 -3
  6. package/dist/adapters/filesystem.d.ts +4 -0
  7. package/dist/adapters/kv.d.ts +10 -0
  8. package/dist/adapters/kv.js +23 -7
  9. package/dist/adapters/local-node.d.ts +4 -6
  10. package/dist/adapters/local-node.js +9 -32
  11. package/dist/adapters/notify.d.ts +12 -0
  12. package/dist/adapters/notify.js +33 -0
  13. package/dist/adapters/query-keys.d.ts +4 -0
  14. package/dist/adapters/query-keys.js +4 -0
  15. package/dist/adapters/services.d.ts +96 -15
  16. package/dist/adapters/services.js +241 -4
  17. package/dist/auth/session.d.ts +66 -0
  18. package/dist/auth/session.js +172 -0
  19. package/dist/catalog/hooks.d.ts +10 -0
  20. package/dist/catalog/hooks.js +82 -11
  21. package/dist/db/migrations.js +274 -0
  22. package/dist/db/operations.d.ts +53 -2
  23. package/dist/db/operations.js +177 -8
  24. package/dist/domain/business/index.d.ts +5 -0
  25. package/dist/domain/business/index.js +5 -0
  26. package/dist/domain/orders/lifecycle.d.ts +6 -0
  27. package/dist/domain/orders/lifecycle.js +6 -0
  28. package/dist/domain/orders/service.d.ts +24 -0
  29. package/dist/domain/orders/service.js +242 -0
  30. package/dist/domain/payments/index.d.ts +6 -0
  31. package/dist/domain/payments/index.js +5 -0
  32. package/dist/domain/reference/index.d.ts +8 -0
  33. package/dist/domain/reference/index.js +8 -0
  34. package/dist/e2e/harness.d.ts +1 -0
  35. package/dist/e2e/harness.js +61 -0
  36. package/dist/hooks/analytics.hooks.d.ts +25 -0
  37. package/dist/hooks/analytics.hooks.js +214 -0
  38. package/dist/hooks/business-hours.hooks.d.ts +8 -0
  39. package/dist/hooks/business-hours.hooks.js +45 -0
  40. package/dist/hooks/dashboard.hooks.d.ts +30 -0
  41. package/dist/hooks/dashboard.hooks.js +132 -0
  42. package/dist/hooks/delivery.hooks.d.ts +225 -0
  43. package/dist/hooks/delivery.hooks.js +80 -0
  44. package/dist/hooks/fleet.hooks.d.ts +37 -0
  45. package/dist/hooks/fleet.hooks.js +229 -0
  46. package/dist/hooks/image.hooks.d.ts +24 -0
  47. package/dist/hooks/image.hooks.js +66 -0
  48. package/dist/hooks/kiosk.hooks.d.ts +22 -5
  49. package/dist/hooks/kiosk.hooks.js +74 -5
  50. package/dist/hooks/members.hooks.d.ts +48 -0
  51. package/dist/hooks/members.hooks.js +302 -0
  52. package/dist/hooks/offline-orders.hooks.d.ts +9 -0
  53. package/dist/hooks/offline-orders.hooks.js +51 -0
  54. package/dist/hooks/orders.hooks.d.ts +18 -39
  55. package/dist/hooks/orders.hooks.js +124 -343
  56. package/dist/hooks/places.hooks.d.ts +21 -0
  57. package/dist/hooks/places.hooks.js +112 -0
  58. package/dist/hooks/realtime-stubs.d.ts +28 -0
  59. package/dist/hooks/realtime-stubs.js +85 -0
  60. package/dist/hooks/rider.hooks.d.ts +470 -0
  61. package/dist/hooks/rider.hooks.js +373 -0
  62. package/dist/hooks/use-infinite-list.d.ts +243 -0
  63. package/dist/hooks/use-infinite-list.js +58 -0
  64. package/dist/hooks/wallet.hooks.d.ts +219 -0
  65. package/dist/hooks/wallet.hooks.js +56 -0
  66. package/dist/image-cache/service.d.ts +28 -0
  67. package/dist/image-cache/service.js +51 -3
  68. package/dist/index.d.ts +56 -6
  69. package/dist/index.js +73 -6
  70. package/dist/local-node/adapters.d.ts +87 -0
  71. package/dist/local-node/adapters.js +4 -0
  72. package/dist/local-node/index.d.ts +5 -0
  73. package/dist/local-node/index.js +5 -0
  74. package/dist/local-node/security.d.ts +25 -0
  75. package/dist/local-node/security.js +149 -0
  76. package/dist/local-node/service.d.ts +83 -0
  77. package/dist/local-node/service.js +706 -0
  78. package/dist/local-node/store.d.ts +50 -0
  79. package/dist/local-node/store.js +284 -0
  80. package/dist/local-node/types.d.ts +92 -0
  81. package/dist/local-node/types.js +4 -0
  82. package/dist/printer/receipt.d.ts +4 -0
  83. package/dist/printer/receipt.js +75 -0
  84. package/dist/printer/service.js +1 -1
  85. package/dist/providers/query-client.d.ts +13 -0
  86. package/dist/providers/query-client.js +58 -0
  87. package/dist/realtime/phoenix-socket.js +30 -1
  88. package/dist/realtime/useEventsConnection.js +11 -2
  89. package/dist/reference/api.d.ts +2 -0
  90. package/dist/reference/api.js +2 -0
  91. package/dist/reference/capabilities.d.ts +12 -7
  92. package/dist/reference/capabilities.js +27 -13
  93. package/dist/reference/hooks.d.ts +34 -0
  94. package/dist/reference/hooks.js +124 -0
  95. package/dist/reference/menu-apply.d.ts +29 -0
  96. package/dist/reference/menu-apply.js +289 -0
  97. package/dist/reference/menu-record.d.ts +69 -0
  98. package/dist/reference/menu-record.js +168 -0
  99. package/dist/reference/operations.d.ts +3 -0
  100. package/dist/reference/operations.js +35 -1
  101. package/dist/reference/service.js +36 -9
  102. package/dist/reference/types.d.ts +1 -1
  103. package/dist/repositories/order.repository.d.ts +7 -0
  104. package/dist/repositories/order.repository.js +15 -2
  105. package/dist/repositories/pos.repository.d.ts +4 -0
  106. package/dist/repositories/pos.repository.js +4 -0
  107. package/dist/sync/backoff.d.ts +21 -0
  108. package/dist/sync/backoff.js +42 -0
  109. package/dist/sync/command-builder.js +10 -2
  110. package/dist/sync/command-queue.service.d.ts +12 -0
  111. package/dist/sync/command-queue.service.js +81 -16
  112. package/dist/sync/device-lease.service.d.ts +3 -3
  113. package/dist/sync/device-lease.service.js +80 -20
  114. package/dist/sync/engine.d.ts +3 -1
  115. package/dist/sync/engine.js +44 -4
  116. package/dist/sync/full-sync.d.ts +6 -0
  117. package/dist/sync/full-sync.js +72 -0
  118. package/dist/sync/health-probe.d.ts +24 -0
  119. package/dist/sync/health-probe.js +138 -0
  120. package/dist/sync/offline-capable.js +25 -0
  121. package/dist/sync/online-gates.d.ts +17 -0
  122. package/dist/sync/online-gates.js +55 -0
  123. package/dist/sync/order-number.d.ts +4 -4
  124. package/dist/sync/order-number.js +33 -13
  125. package/dist/sync/order-snapshots.service.js +18 -3
  126. package/dist/sync/pending-orders.service.d.ts +18 -0
  127. package/dist/sync/pending-orders.service.js +67 -1
  128. package/dist/sync/reconciliation/index.d.ts +5 -0
  129. package/dist/sync/reconciliation/index.js +5 -0
  130. package/dist/sync/snapshots/entity.d.ts +6 -0
  131. package/dist/sync/snapshots/entity.js +6 -0
  132. package/dist/sync/sync-jobs.service.d.ts +18 -0
  133. package/dist/sync/sync-jobs.service.js +539 -0
  134. package/dist/sync/transition-endpoints.d.ts +11 -0
  135. package/dist/sync/transition-endpoints.js +30 -0
  136. package/dist/sync/transport-local-node.js +3 -2
  137. package/dist/sync/transport-rest.d.ts +7 -0
  138. package/dist/sync/transport-rest.js +84 -26
  139. package/dist/sync/use-sync-reconciliation.js +15 -1
  140. package/dist/test-helpers/memory-driver.js +182 -4
  141. package/dist/test-helpers/seed-hours.d.ts +1 -0
  142. package/dist/test-helpers/seed-hours.js +23 -0
  143. package/dist/types/api/business.api.d.ts +42 -0
  144. package/dist/types/api/menu.api.d.ts +15 -0
  145. package/dist/types/api/seller.api.d.ts +20 -0
  146. package/dist/types/places.d.ts +19 -0
  147. package/dist/types/places.js +2 -0
  148. package/dist/types/rider.d.ts +67 -0
  149. package/dist/types/rider.js +2 -0
  150. package/dist/utils/attempt-stats.d.ts +23 -0
  151. package/dist/utils/attempt-stats.js +61 -0
  152. package/dist/utils/business-hours.d.ts +37 -0
  153. package/dist/utils/business-hours.js +96 -0
  154. package/dist/utils/debug-log.d.ts +23 -0
  155. package/dist/utils/debug-log.js +153 -0
  156. package/dist/utils/order-filter.d.ts +1 -1
  157. package/dist/utils/order-filter.js +1 -1
  158. package/dist/utils/order-transitions.d.ts +30 -0
  159. package/dist/utils/order-transitions.js +185 -0
  160. package/dist/utils/sync-logger.js +5 -4
  161. package/dist/validation/offline.d.ts +46 -0
  162. package/dist/validation/offline.js +79 -0
  163. package/package.json +6 -1
  164. package/dist/pricing/engine.test.d.ts +0 -1
  165. package/dist/pricing/engine.test.js +0 -142
  166. package/dist/reference/lww.test.d.ts +0 -1
  167. package/dist/reference/lww.test.js +0 -23
  168. package/dist/reference/operations.test.d.ts +0 -1
  169. package/dist/reference/operations.test.js +0 -66
  170. package/dist/standalone.integration.test.d.ts +0 -1
  171. package/dist/standalone.integration.test.js +0 -235
  172. package/dist/sync/device-lease.test.d.ts +0 -1
  173. package/dist/sync/device-lease.test.js +0 -38
  174. package/dist/sync/offline-integration.test.d.ts +0 -1
  175. package/dist/sync/offline-integration.test.js +0 -52
  176. package/dist/sync/order-number.test.d.ts +0 -1
  177. package/dist/sync/order-number.test.js +0 -51
  178. package/dist/sync/perms.test.d.ts +0 -1
  179. package/dist/sync/perms.test.js +0 -15
  180. package/dist/utils/currency.test.d.ts +0 -1
  181. package/dist/utils/currency.test.js +0 -17
  182. package/dist/utils/hlc.test.d.ts +0 -1
  183. package/dist/utils/hlc.test.js +0 -29
  184. package/dist/utils/phone.test.d.ts +0 -1
  185. package/dist/utils/phone.test.js +0 -19
  186. package/dist/utils/responsive.d.ts +0 -13
  187. package/dist/utils/responsive.js +0 -18
  188. package/dist/utils/uuid.test.d.ts +0 -1
  189. package/dist/utils/uuid.test.js +0 -13
  190. package/dist/utils/validation.test.d.ts +0 -1
  191. package/dist/utils/validation.test.js +0 -23
package/dist/index.js CHANGED
@@ -7,6 +7,9 @@
7
7
  // ── Types ──
8
8
  export * from "./types/api";
9
9
  export * from "./types/pos.types";
10
+ export * from "./types/rider";
11
+ export * from "./types/places";
12
+ export * from "./adapters/notify";
10
13
  // ── Pricing (server-parity) ──
11
14
  export { calculatePricing, calculateTaxLines, calculateExclusiveTax, calculateInclusiveTaxShare, } from "./pricing/engine";
12
15
  // ── Utils ──
@@ -15,14 +18,29 @@ export * from "./utils/currency";
15
18
  export * from "./utils/error";
16
19
  export * from "./utils/uuid";
17
20
  export * from "./utils/sync-logger";
21
+ export * from "./utils/debug-log";
22
+ export * from "./utils/attempt-stats";
18
23
  export { PHONE_COUNTRIES, getDialCodeFromCountry, getCountryOption, normalizePhoneForCountry, isLikelyE164 } from "./utils/phone";
19
24
  export * from "./utils/formatCurrency";
20
25
  export { formatDate, formatRelativeTime } from "./utils/formatDate";
21
26
  export * from "./utils/validation";
22
- export * from "./utils/responsive";
27
+ export * from "./validation/offline";
23
28
  export * from "./utils/copyToClipBoard";
24
29
  export * from "./utils/customer-display";
25
30
  export * from "./utils/order-filter";
31
+ export { useReferenceData } from "./reference/hooks";
32
+ export { setNotifyHandlers } from "./adapters/notify";
33
+ export { walletService, riderService, fleetService, deliveryService, dashboardService, analyticsService, placesService } from "./adapters/services";
34
+ export { buildMenuItemEnvelope, buildMenuCategoryEnvelope } from "./reference/menu-record";
35
+ export { validateMenuItemInput, validateMenuCategoryInput, validateOrderCreateInput, validateInviteInput } from "./validation/offline";
36
+ export { useBusinessDeliveriesQuery } from "./hooks/delivery.hooks";
37
+ export { useBusinessReferenceDataSync } from "./reference/hooks";
38
+ export { createAppQueryClient, createQueryPersister } from "./providers/query-client";
39
+ export { useSellerAnalyticsQuery } from "./hooks/analytics.hooks";
40
+ export { useSellerDashboardQuery } from "./hooks/dashboard.hooks";
41
+ export { useRealtimeInvalidation, useOrderRealtimeUpdates } from "./hooks/realtime-stubs";
42
+ export { useAcceptFleetInvitationMutation, useAcceptBusinessInvitationMutation, useUserInvitationsQuery, } from "./hooks/members.hooks";
43
+ export { useJoinRequestsQuery } from "./hooks/fleet.hooks";
26
44
  // ── DB ──
27
45
  export * from "./db/migrations";
28
46
  export * from "./db/operations";
@@ -30,10 +48,10 @@ export * as dbOps from "./db/operations";
30
48
  export { openSyncDatabase, getSyncDatabase, closeSyncDatabase } from "./db/connection";
31
49
  // ── Sync ──
32
50
  export * from "./sync/types";
51
+ export * from "./sync/online-gates";
33
52
  export * from "./sync/use-sync-reconciliation";
34
53
  export { SyncEngine, syncEngine } from "./sync/engine";
35
54
  export * from "./sync/command-builder";
36
- export * from "./sync/command-queue.service";
37
55
  export { commandQueueService } from "./sync/command-queue.service";
38
56
  export { buildCommand } from "./sync/command-builder";
39
57
  export { generateLocalOrderNumber, generateLocalCalloutNumber, isLocalOrderNumber, generateOrderNumber, generateOrderNumberForBusiness, isOfflineOrderNumber, parseOrderNumber, resetCalloutForTests, } from "./sync/order-number";
@@ -55,30 +73,54 @@ export { orderRepository } from "./repositories/order.repository";
55
73
  export { posRepository } from "./repositories/pos.repository";
56
74
  // ── Order lifecycle hooks (offline-first, device-owned outbox) ──
57
75
  export * from "./hooks/orders.hooks";
76
+ export * from "./hooks/use-infinite-list";
77
+ export * from "./hooks/wallet.hooks";
78
+ export * from "./hooks/rider.hooks";
79
+ export * from "./hooks/fleet.hooks";
80
+ export * from "./hooks/delivery.hooks";
81
+ export * from "./hooks/dashboard.hooks";
82
+ export * from "./hooks/analytics.hooks";
83
+ export * from "./hooks/members.hooks";
84
+ export * from "./hooks/places.hooks";
85
+ export * from "./hooks/offline-orders.hooks";
86
+ export * from "./hooks/image.hooks";
87
+ export * from "./hooks/realtime-stubs";
88
+ export * from "./providers/query-client";
89
+ export { KIOSK_KEYS, useKioskOrdersQuery, useAvailableRidersQuery, useAcceptKioskOrderMutation, useRejectKioskOrderMutation, useStartPreparingKioskOrderMutation, useMarkKioskOrderReadyMutation, } from "./hooks/kiosk.hooks";
58
90
  // ── Reference ──
59
91
  export * from "./reference/types";
60
92
  export * from "./reference/operations";
61
- export { fetchReferenceVersions, fetchReferenceData, fetchReferenceChanges, setReferenceLocalNodeFetcher } from "./reference/api";
93
+ export { fetchReferenceVersions, fetchReferenceData, fetchReferenceChanges, setReferenceLocalNodeFetcher, setReferenceData, mergeReferenceData, setOperatingHoursLocal, setBusinessProfileLocal } from "./reference/api";
62
94
  export { emitReferenceChanged, applyReferenceChangeEvent, syncReferenceDataOnReconnect, registerReferenceEventEngine, registerReferenceInvalidator } from "./reference/events";
63
95
  export * from "./reference/events";
64
96
  export * from "./reference/service";
97
+ export * from "./reference/hooks";
65
98
  export { referenceSyncService } from "./reference/service";
99
+ export * from "./reference/menu-record";
100
+ export * from "./reference/menu-apply";
66
101
  export * from "./reference/capabilities";
67
102
  // ── Adapters (for host apps to supply) ──
68
103
  export * from "./adapters/sqlite";
69
104
  export { setKVDriver, getKVDriver, kvGetItem, kvSetItem, kvRemoveItem, getItem, setItem, removeItem, getItemSync, setItemSync } from "./adapters/kv";
70
105
  export * from "./adapters/connectivity";
71
106
  export * from "./adapters/http";
72
- export * from "./adapters/local-node";
107
+ export * from "./local-node/types";
108
+ export * from "./local-node/store";
109
+ export * from "./local-node/security";
110
+ export * from "./local-node/adapters";
111
+ export * from "./local-node/service";
112
+ // adapters/local-node is compat shim — export only non-conflicting
113
+ export { setLocalNodeState } from "./adapters/local-node";
73
114
  export * from "./adapters/services";
74
115
  export * from "./adapters/devices";
75
- export { setAuthProvider, getAuthToken } from "./adapters/auth";
116
+ export { setAuthProvider, getAuthToken, setAuthRefreshProvider, getRefreshToken, setAuthTokensProvider, setAuthTokens, setAuthClearProvider, clearAuth } from "./adapters/auth";
76
117
  export { imageCacheService } from "./adapters/image-cache";
77
118
  export * from "./adapters/business";
78
119
  export * from "./adapters/pending-orders";
79
120
  export * from "./adapters/print";
80
121
  export * from "./adapters/filesystem";
81
- export { KIOSK_KEYS, SELLER_ORDERS_KEYS, BUSINESS_DELIVERIES_KEYS, MENU_KEYS, REFERENCE_KEYS } from "./adapters/query-keys";
122
+ export { SELLER_ORDERS_KEYS, BUSINESS_DELIVERIES_KEYS, MENU_KEYS, REFERENCE_KEYS } from "./adapters/query-keys";
123
+ // KIOSK_KEYS exported from hooks/kiosk.hooks (single source)
82
124
  // POS_KEYS / SELLER_KEYS are exported from hooks/orders.hooks (offline-first source of truth)
83
125
  // ── Printer (backend-identical template) ──
84
126
  export * from "./printer/types";
@@ -91,11 +133,26 @@ export * from "./cart/store";
91
133
  export * from "./cart/hooks";
92
134
  // ── Catalog ──
93
135
  export * from "./catalog/hooks";
136
+ // ── Order transitions (shared business logic — single source) ──
137
+ export * from "./utils/order-transitions";
138
+ // ── Business hours gate (single source — mirrors backend is_open?) ──
139
+ export * from "./utils/business-hours";
140
+ export * from "./hooks/business-hours.hooks";
141
+ // components/OrderStatusCard was folded into order-transitions (compat re-export lives in apps)
142
+ // ── Domain (offline-first primary DB) ──
143
+ export * from "./domain/orders/service";
94
144
  // ── Realtime (single source — PHX WebSocket + business:{id}) ──
95
145
  export { phoenixSocket } from "./realtime/phoenix-socket";
96
146
  export { eventsService } from "./realtime/events.service";
97
147
  export { orderEventsService } from "./realtime/order-events.service";
98
148
  export { useEventsConnection } from "./realtime/useEventsConnection";
149
+ // ── Sync jobs (POS primary sync) — debug helpers for DiagnosticsScreen
150
+ export { getSyncJobsPendingCount, debugDumpSyncJobs, drainPending } from "./sync/sync-jobs.service";
151
+ export { performFullSync } from "./sync/full-sync";
152
+ export { stopHealthProbe } from "./sync/health-probe";
153
+ export { notifyAuthChanged, subscribeAuth } from "./adapters/auth";
154
+ // ── Session lifecycle (access-expired → refresh → refresh-expired → logout)
155
+ export { ACCESS_EXPIRED_CODE, SESSION_DEAD_CODES, SKIP_AUTH_RETRY, extractAuthFailure, isAccessExpired, isSessionDead, hasAuthRetrySkip, refreshSession, withAuthRetry, } from "./auth/session";
99
156
  // ── Image cache (durable) ──
100
157
  export { getCachedImagePath, ensureImageCacheDir, getCachedImageInfo, cacheRemoteImage, cacheImageBatch, clearImageCache, pruneImageCache, resolveImageSource, stableCacheUrl } from "./image-cache/service";
101
158
  // ── Init helper ──
@@ -109,6 +166,8 @@ import { API_BASE_URL, WS_BASE_URL, setApiBaseUrl, setWsBaseUrl } from "./utils/
109
166
  import { SyncEngine } from "./sync/engine";
110
167
  import { restTransport } from "./sync/transport-rest";
111
168
  import { localNodeTransport } from "./sync/transport-local-node";
169
+ import { setupSyncJobsDrain } from "./sync/sync-jobs.service";
170
+ import { startHealthProbe } from "./sync/health-probe";
112
171
  let engine = null;
113
172
  export function createTeincCore(config = {}) {
114
173
  try {
@@ -154,6 +213,14 @@ export function createTeincCore(config = {}) {
154
213
  }
155
214
  if (config.initialConnectivity)
156
215
  setConnectivityState(config.initialConnectivity);
216
+ try {
217
+ setupSyncJobsDrain();
218
+ }
219
+ catch { }
220
+ try {
221
+ startHealthProbe();
222
+ }
223
+ catch { }
157
224
  // Debug: confirm env vars as seen by core after injection
158
225
  try {
159
226
  console.debug("[core] final URLs", { API_BASE_URL, WS_BASE_URL });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Local Node adapters — platform drivers implement these.
3
+ */
4
+ import type { Command, DomainEvent } from "../sync/types";
5
+ import type { ReferenceDataKey, ReferenceDataRecord } from "../reference/types";
6
+ import type { DiscoveredNode } from "./store";
7
+ export interface DiscoveryAdapter {
8
+ setOptions(opts: {
9
+ deviceId: string;
10
+ deviceName?: string;
11
+ onNodeFound: (node: DiscoveredNode) => void;
12
+ onNodeLost: (deviceId: string) => void;
13
+ }): void;
14
+ startScanning(): void;
15
+ stopScanning(): void;
16
+ startPublishing(opts: {
17
+ deviceId: string;
18
+ deviceName?: string;
19
+ port: number;
20
+ onNodeFound: (node: DiscoveredNode) => void;
21
+ onNodeLost: (deviceId: string) => void;
22
+ }): void;
23
+ stop(): void;
24
+ }
25
+ export interface LocalNodeServer {
26
+ start(): Promise<void>;
27
+ stop(): Promise<void>;
28
+ isListening(): boolean;
29
+ broadcast(event: DomainEvent): void;
30
+ }
31
+ export interface LocalNodeClient {
32
+ connect(): Promise<void>;
33
+ disconnect(): void;
34
+ isConnected(): boolean;
35
+ execute(command: Command): Promise<DomainEvent[]>;
36
+ requestReferenceData(businessId: string, keys: ReferenceDataKey[]): Promise<{
37
+ data: Partial<Record<ReferenceDataKey, ReferenceDataRecord | null>>;
38
+ }>;
39
+ publishEvent?(event: DomainEvent): Promise<void> | void;
40
+ }
41
+ export interface TransportAdapter {
42
+ createServer(opts: {
43
+ port: number;
44
+ businessId: string;
45
+ secret: string;
46
+ onCommand: (command: Command) => Promise<DomainEvent[]>;
47
+ onRelayEvent: (event: DomainEvent) => void;
48
+ onClientConnected: (info: {
49
+ deviceId: string;
50
+ deviceName?: string;
51
+ host?: string;
52
+ }) => void;
53
+ onClientDisconnected: (deviceId: string) => void;
54
+ }): LocalNodeServer;
55
+ createClient(opts: {
56
+ node: {
57
+ deviceId: string;
58
+ host: string;
59
+ port: number;
60
+ };
61
+ businessId: string;
62
+ secret: string;
63
+ onEvent: (event: DomainEvent) => void;
64
+ onConnected: () => void;
65
+ onDisconnected: () => void;
66
+ onError: (error: unknown) => void;
67
+ }): LocalNodeClient;
68
+ }
69
+ export interface KeepAliveAdapter {
70
+ activate(tag: string): Promise<void>;
71
+ deactivate(tag: string): Promise<void>;
72
+ }
73
+ export interface LocalNodeDeps {
74
+ getDeviceId: () => string | Promise<string>;
75
+ getDeviceName?: () => string | undefined;
76
+ getBusiness: () => {
77
+ id: string;
78
+ } | null;
79
+ getAuth: () => {
80
+ user: unknown | null;
81
+ accessToken: string | null;
82
+ };
83
+ discovery: DiscoveryAdapter;
84
+ transport: TransportAdapter;
85
+ keepAlive?: KeepAliveAdapter;
86
+ cloudCommandTimeoutMs?: number;
87
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Local Node adapters — platform drivers implement these.
3
+ */
4
+ export {};
@@ -0,0 +1,5 @@
1
+ export * from "./types";
2
+ export * from "./security";
3
+ export * from "./store";
4
+ export * from "./adapters";
5
+ export * from "./service";
@@ -0,0 +1,5 @@
1
+ export * from "./types";
2
+ export * from "./security";
3
+ export * from "./store";
4
+ export * from "./adapters";
5
+ export * from "./service";
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Local Node security helpers — platform-agnostic.
3
+ *
4
+ * SINGLE OWNER of mesh crypto (formula + inputs). Apps (Business/Desktop/Rust)
5
+ * MUST NOT derive or re-implement this: core derives the secret once per
6
+ * business and hands the opaque value to transports. Transports only carry it.
7
+ *
8
+ * Mesh decision: the secret is business-only — `SHA256("teinc-lan:{id}:")`
9
+ * with a permanently empty owner segment. Same business_id ⇒ same secret on
10
+ * every device, regardless of which owner fields each app's store happens to
11
+ * carry (that variance caused `invalid_proof` between devices). The trailing
12
+ * colon is kept so the wire value is byte-identical to the historic formula.
13
+ *
14
+ * Pure JS WebCrypto/Node fallback so core does not depend on expo-crypto.
15
+ */
16
+ /**
17
+ * Self-contained SHA-256 (FIPS 180-4), no platform dependency. Slower than
18
+ * native but handshake-rate only. Pinned by vectors in security.test.ts.
19
+ */
20
+ export declare function sha256Pure(data: Uint8Array): Uint8Array;
21
+ export declare function deriveLanSecret(business: {
22
+ id: string;
23
+ }): Promise<string>;
24
+ export declare function generateNonce(): Promise<string>;
25
+ export declare function computeProof(secret: string, nonce: string): Promise<string>;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Local Node security helpers — platform-agnostic.
3
+ *
4
+ * SINGLE OWNER of mesh crypto (formula + inputs). Apps (Business/Desktop/Rust)
5
+ * MUST NOT derive or re-implement this: core derives the secret once per
6
+ * business and hands the opaque value to transports. Transports only carry it.
7
+ *
8
+ * Mesh decision: the secret is business-only — `SHA256("teinc-lan:{id}:")`
9
+ * with a permanently empty owner segment. Same business_id ⇒ same secret on
10
+ * every device, regardless of which owner fields each app's store happens to
11
+ * carry (that variance caused `invalid_proof` between devices). The trailing
12
+ * colon is kept so the wire value is byte-identical to the historic formula.
13
+ *
14
+ * Pure JS WebCrypto/Node fallback so core does not depend on expo-crypto.
15
+ */
16
+ const LAN_SECRET_SALT = "teinc-lan";
17
+ function bytesToHex(bytes) {
18
+ return Array.from(bytes)
19
+ .map((b) => b.toString(16).padStart(2, "0"))
20
+ .join("");
21
+ }
22
+ function hexToBytes(hex) {
23
+ const out = new Uint8Array(hex.length / 2);
24
+ for (let i = 0; i < out.length; i++)
25
+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
26
+ return out;
27
+ }
28
+ async function sha256Hex(input) {
29
+ const data = new TextEncoder().encode(input);
30
+ // 1) WebCrypto (RN Hermes may expose global crypto.subtle, Tauri does)
31
+ try {
32
+ const g = globalThis;
33
+ const subtle = g?.crypto?.subtle;
34
+ if (subtle) {
35
+ const buf = await subtle.digest("SHA-256", data);
36
+ return bytesToHex(new Uint8Array(buf));
37
+ }
38
+ }
39
+ catch { }
40
+ // 2) Node crypto (tests / desktop node)
41
+ try {
42
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
43
+ const nodeCrypto = require("crypto");
44
+ if (nodeCrypto?.createHash)
45
+ return nodeCrypto.createHash("sha256").update(data).digest("hex");
46
+ }
47
+ catch { }
48
+ // 3) Pure-JS SHA-256 (Hermes/RN has neither subtle nor node crypto).
49
+ // MUST be real SHA-256 — proofs must verify cross-platform, and the old
50
+ // DJB placeholder produced zero-padded non-hashes that silently failed
51
+ // every handshake. No app action needed: identical output everywhere.
52
+ return bytesToHex(sha256Pure(data));
53
+ }
54
+ /**
55
+ * Self-contained SHA-256 (FIPS 180-4), no platform dependency. Slower than
56
+ * native but handshake-rate only. Pinned by vectors in security.test.ts.
57
+ */
58
+ export function sha256Pure(data) {
59
+ const K = [
60
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
61
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
62
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
63
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
64
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
65
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
66
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
67
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
68
+ ];
69
+ let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a;
70
+ let h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19;
71
+ const bitLen = data.length * 8;
72
+ const paddedLen = (((data.length + 8) >> 6) + 1) << 6;
73
+ const msg = new Uint8Array(paddedLen);
74
+ msg.set(data);
75
+ msg[data.length] = 0x80;
76
+ const dv = new DataView(msg.buffer);
77
+ dv.setUint32(paddedLen - 4, bitLen >>> 0, false);
78
+ dv.setUint32(paddedLen - 8, Math.floor(bitLen / 0x100000000), false);
79
+ const w = new Int32Array(64);
80
+ const rotr = (x, n) => (x >>> n) | (x << (32 - n));
81
+ for (let off = 0; off < paddedLen; off += 64) {
82
+ for (let i = 0; i < 16; i++)
83
+ w[i] = dv.getInt32(off + i * 4, false);
84
+ for (let i = 16; i < 64; i++) {
85
+ const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
86
+ const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
87
+ w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0;
88
+ }
89
+ let [a, b, c, d, e, f, g, h] = [h0, h1, h2, h3, h4, h5, h6, h7];
90
+ for (let i = 0; i < 64; i++) {
91
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
92
+ const ch = (e & f) ^ (~e & g);
93
+ const t1 = (h + S1 + ch + K[i] + w[i]) | 0;
94
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
95
+ const maj = (a & b) ^ (a & c) ^ (b & c);
96
+ const t2 = (S0 + maj) | 0;
97
+ h = g;
98
+ g = f;
99
+ f = e;
100
+ e = (d + t1) | 0;
101
+ d = c;
102
+ c = b;
103
+ b = a;
104
+ a = (t1 + t2) | 0;
105
+ }
106
+ h0 = (h0 + a) | 0;
107
+ h1 = (h1 + b) | 0;
108
+ h2 = (h2 + c) | 0;
109
+ h3 = (h3 + d) | 0;
110
+ h4 = (h4 + e) | 0;
111
+ h5 = (h5 + f) | 0;
112
+ h6 = (h6 + g) | 0;
113
+ h7 = (h7 + h) | 0;
114
+ }
115
+ const out = new Uint8Array(32);
116
+ const odv = new DataView(out.buffer);
117
+ [h0, h1, h2, h3, h4, h5, h6, h7].forEach((v, i) => odv.setInt32(i * 4, v, false));
118
+ return out;
119
+ }
120
+ export async function deriveLanSecret(business) {
121
+ return sha256Hex(`${LAN_SECRET_SALT}:${business.id}:`);
122
+ }
123
+ export async function generateNonce() {
124
+ try {
125
+ const g = globalThis;
126
+ const cryptoObj = g?.crypto;
127
+ if (cryptoObj?.getRandomValues) {
128
+ const b = new Uint8Array(16);
129
+ cryptoObj.getRandomValues(b);
130
+ return bytesToHex(b);
131
+ }
132
+ }
133
+ catch { }
134
+ try {
135
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
136
+ const nodeCrypto = require("crypto");
137
+ if (nodeCrypto?.randomBytes)
138
+ return bytesToHex(nodeCrypto.randomBytes(16));
139
+ }
140
+ catch { }
141
+ // fallback Math.random
142
+ const b = new Uint8Array(16);
143
+ for (let i = 0; i < 16; i++)
144
+ b[i] = Math.floor(Math.random() * 256);
145
+ return bytesToHex(b);
146
+ }
147
+ export async function computeProof(secret, nonce) {
148
+ return sha256Hex(`${nonce}:${secret}`);
149
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Local Node Service — mesh, no elected, every isLocalNodeEnabled device is both server and client.
3
+ */
4
+ import type { Command, DomainEvent } from "../sync/types";
5
+ import type { LocalNodeInfo } from "./types";
6
+ import type { LocalNodeDeps, LocalNodeClient } from "./adapters";
7
+ import type { ReferenceDataKey } from "../reference/types";
8
+ declare class LocalNodeServiceCore {
9
+ private server;
10
+ private peers;
11
+ private peerInfos;
12
+ private mode;
13
+ private businessId;
14
+ private secret;
15
+ private subscribed;
16
+ private cachedDeviceId;
17
+ private lifecyclePromise;
18
+ private starting;
19
+ private deps;
20
+ /** mDNS publish state — a failed publish must never abort discovery. */
21
+ private published;
22
+ /** Whether discovery scanning was successfully started. */
23
+ private discovering;
24
+ init(deps: LocalNodeDeps): void;
25
+ private getDeps;
26
+ private getDeviceId;
27
+ private runLifecycle;
28
+ private runInitialize;
29
+ initialize(): Promise<void>;
30
+ shutdown(): Promise<void>;
31
+ reconnectNow(): void;
32
+ private runShutdown;
33
+ isLocalNode(): boolean;
34
+ isConnectedToNode(): boolean;
35
+ /** Mesh legibility for Diagnostics screens: publish/scan state per leg. */
36
+ getMeshStatus(): {
37
+ mode: string;
38
+ listening: boolean;
39
+ published: boolean;
40
+ discovering: boolean;
41
+ peers: number;
42
+ };
43
+ getClient(): LocalNodeClient | null;
44
+ getElectedNode(): LocalNodeInfo | null;
45
+ broadcastToClients(event: DomainEvent): void;
46
+ private storeUnsub;
47
+ private setupStoreSubscriptions;
48
+ private teardownStoreSubscriptions;
49
+ private startAsNode;
50
+ /**
51
+ * (Re)publish this node's mDNS advertisement. Isolated from server bring-up
52
+ * and discovery: a publish throw must never abort scanning, sweeping, or
53
+ * peer dials. Safe to call repeatedly (drivers dedupe internally).
54
+ */
55
+ private ensurePublished;
56
+ private startDiscovery;
57
+ /** Start scanning if not already running (idempotent drivers ignore repeats). */
58
+ private ensureDiscovery;
59
+ private dialInFlight;
60
+ private dialTimers;
61
+ private sweepTimer;
62
+ /** deviceId -> timestamp of last business_mismatch rejection. Foreign-business
63
+ * peers (test rigs, neighboring shops) would otherwise be redialed on every
64
+ * discovery event; back off quietly and re-probe after the window. */
65
+ private mismatchBackoff;
66
+ private static readonly MISMATCH_BACKOFF_MS;
67
+ private startPeerSweep;
68
+ private stopPeerSweep;
69
+ private connectToPeer;
70
+ private settleDial;
71
+ handleNodeCommand(command: Command): Promise<DomainEvent | null>;
72
+ requestReferenceData(businessId: string, keys: ReferenceDataKey[]): Promise<Partial<Record<ReferenceDataKey, import("../reference/types").ReferenceDataRecord | null>> | null>;
73
+ getState(): {
74
+ isLocalNode: boolean;
75
+ isConnectedToNode: boolean;
76
+ electedNode: LocalNodeInfo | null;
77
+ client: LocalNodeClient | null;
78
+ };
79
+ }
80
+ export declare const localNodeService: LocalNodeServiceCore;
81
+ export declare function initLocalNodeService(deps: LocalNodeDeps): LocalNodeServiceCore;
82
+ export type LocalNodeService = LocalNodeServiceCore;
83
+ export {};