@entifix/next-shell 0.1.0-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 (271) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/dist/index.d.ts +21 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +19 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/lib/back-office/account-menu.d.ts +29 -0
  8. package/dist/lib/back-office/account-menu.d.ts.map +1 -0
  9. package/dist/lib/back-office/account-menu.js +70 -0
  10. package/dist/lib/back-office/account-menu.js.map +1 -0
  11. package/dist/lib/back-office/back-office-shell.d.ts +50 -0
  12. package/dist/lib/back-office/back-office-shell.d.ts.map +1 -0
  13. package/dist/lib/back-office/back-office-shell.js +128 -0
  14. package/dist/lib/back-office/back-office-shell.js.map +1 -0
  15. package/dist/lib/back-office/breadcrumbs.d.ts +16 -0
  16. package/dist/lib/back-office/breadcrumbs.d.ts.map +1 -0
  17. package/dist/lib/back-office/breadcrumbs.js +53 -0
  18. package/dist/lib/back-office/breadcrumbs.js.map +1 -0
  19. package/dist/lib/back-office/group-by-screen-type.d.ts +21 -0
  20. package/dist/lib/back-office/group-by-screen-type.d.ts.map +1 -0
  21. package/dist/lib/back-office/group-by-screen-type.js +39 -0
  22. package/dist/lib/back-office/group-by-screen-type.js.map +1 -0
  23. package/dist/lib/back-office/index.d.ts +6 -0
  24. package/dist/lib/back-office/index.d.ts.map +1 -0
  25. package/dist/lib/back-office/index.js +7 -0
  26. package/dist/lib/back-office/index.js.map +1 -0
  27. package/dist/lib/back-office/nav.d.ts +39 -0
  28. package/dist/lib/back-office/nav.d.ts.map +1 -0
  29. package/dist/lib/back-office/nav.js +3 -0
  30. package/dist/lib/back-office/nav.js.map +1 -0
  31. package/dist/lib/back-office/sidebar-nav.d.ts +26 -0
  32. package/dist/lib/back-office/sidebar-nav.d.ts.map +1 -0
  33. package/dist/lib/back-office/sidebar-nav.js +181 -0
  34. package/dist/lib/back-office/sidebar-nav.js.map +1 -0
  35. package/dist/lib/command-palette/command-palette-host.d.ts +41 -0
  36. package/dist/lib/command-palette/command-palette-host.d.ts.map +1 -0
  37. package/dist/lib/command-palette/command-palette-host.js +196 -0
  38. package/dist/lib/command-palette/command-palette-host.js.map +1 -0
  39. package/dist/lib/command-palette/index.d.ts +9 -0
  40. package/dist/lib/command-palette/index.d.ts.map +1 -0
  41. package/dist/lib/command-palette/index.js +10 -0
  42. package/dist/lib/command-palette/index.js.map +1 -0
  43. package/dist/lib/command-palette/palette-command.d.ts +32 -0
  44. package/dist/lib/command-palette/palette-command.d.ts.map +1 -0
  45. package/dist/lib/command-palette/palette-command.js +19 -0
  46. package/dist/lib/command-palette/palette-command.js.map +1 -0
  47. package/dist/lib/command-palette/use-command-recency.d.ts +28 -0
  48. package/dist/lib/command-palette/use-command-recency.d.ts.map +1 -0
  49. package/dist/lib/command-palette/use-command-recency.js +67 -0
  50. package/dist/lib/command-palette/use-command-recency.js.map +1 -0
  51. package/dist/lib/command-palette/use-command-source.d.ts +17 -0
  52. package/dist/lib/command-palette/use-command-source.d.ts.map +1 -0
  53. package/dist/lib/command-palette/use-command-source.js +70 -0
  54. package/dist/lib/command-palette/use-command-source.js.map +1 -0
  55. package/dist/lib/command-palette/use-nav-source.d.ts +21 -0
  56. package/dist/lib/command-palette/use-nav-source.d.ts.map +1 -0
  57. package/dist/lib/command-palette/use-nav-source.js +78 -0
  58. package/dist/lib/command-palette/use-nav-source.js.map +1 -0
  59. package/dist/lib/command-palette/use-records-source.d.ts +23 -0
  60. package/dist/lib/command-palette/use-records-source.d.ts.map +1 -0
  61. package/dist/lib/command-palette/use-records-source.js +199 -0
  62. package/dist/lib/command-palette/use-records-source.js.map +1 -0
  63. package/dist/lib/command-palette/use-tabs-source.d.ts +20 -0
  64. package/dist/lib/command-palette/use-tabs-source.d.ts.map +1 -0
  65. package/dist/lib/command-palette/use-tabs-source.js +52 -0
  66. package/dist/lib/command-palette/use-tabs-source.js.map +1 -0
  67. package/dist/lib/command-palette/use-use-cases-source.d.ts +22 -0
  68. package/dist/lib/command-palette/use-use-cases-source.d.ts.map +1 -0
  69. package/dist/lib/command-palette/use-use-cases-source.js +93 -0
  70. package/dist/lib/command-palette/use-use-cases-source.js.map +1 -0
  71. package/dist/lib/config/config-route.d.ts +51 -0
  72. package/dist/lib/config/config-route.d.ts.map +1 -0
  73. package/dist/lib/config/config-route.js +65 -0
  74. package/dist/lib/config/config-route.js.map +1 -0
  75. package/dist/lib/config/service-proxy-route.d.ts +28 -0
  76. package/dist/lib/config/service-proxy-route.d.ts.map +1 -0
  77. package/dist/lib/config/service-proxy-route.js +125 -0
  78. package/dist/lib/config/service-proxy-route.js.map +1 -0
  79. package/dist/lib/config/service-token.d.ts +5 -0
  80. package/dist/lib/config/service-token.d.ts.map +1 -0
  81. package/dist/lib/config/service-token.js +18 -0
  82. package/dist/lib/config/service-token.js.map +1 -0
  83. package/dist/lib/crud/entity-crud-form.d.ts +49 -0
  84. package/dist/lib/crud/entity-crud-form.d.ts.map +1 -0
  85. package/dist/lib/crud/entity-crud-form.js +133 -0
  86. package/dist/lib/crud/entity-crud-form.js.map +1 -0
  87. package/dist/lib/crud/hand-off-write.d.ts +35 -0
  88. package/dist/lib/crud/hand-off-write.d.ts.map +1 -0
  89. package/dist/lib/crud/hand-off-write.js +27 -0
  90. package/dist/lib/crud/hand-off-write.js.map +1 -0
  91. package/dist/lib/crud/index.d.ts +9 -0
  92. package/dist/lib/crud/index.d.ts.map +1 -0
  93. package/dist/lib/crud/index.js +10 -0
  94. package/dist/lib/crud/index.js.map +1 -0
  95. package/dist/lib/crud/make-entity-crud.d.ts +28 -0
  96. package/dist/lib/crud/make-entity-crud.d.ts.map +1 -0
  97. package/dist/lib/crud/make-entity-crud.js +318 -0
  98. package/dist/lib/crud/make-entity-crud.js.map +1 -0
  99. package/dist/lib/crud/make-entity-crud.types.d.ts +237 -0
  100. package/dist/lib/crud/make-entity-crud.types.d.ts.map +1 -0
  101. package/dist/lib/crud/make-entity-crud.types.js +6 -0
  102. package/dist/lib/crud/make-entity-crud.types.js.map +1 -0
  103. package/dist/lib/crud/pending-notice.d.ts +31 -0
  104. package/dist/lib/crud/pending-notice.d.ts.map +1 -0
  105. package/dist/lib/crud/pending-notice.js +72 -0
  106. package/dist/lib/crud/pending-notice.js.map +1 -0
  107. package/dist/lib/crud/slug.d.ts +13 -0
  108. package/dist/lib/crud/slug.d.ts.map +1 -0
  109. package/dist/lib/crud/slug.js +13 -0
  110. package/dist/lib/crud/slug.js.map +1 -0
  111. package/dist/lib/crud/use-entity-affordances.d.ts +20 -0
  112. package/dist/lib/crud/use-entity-affordances.d.ts.map +1 -0
  113. package/dist/lib/crud/use-entity-affordances.js +22 -0
  114. package/dist/lib/crud/use-entity-affordances.js.map +1 -0
  115. package/dist/lib/crud/use-entity-bulk.d.ts +43 -0
  116. package/dist/lib/crud/use-entity-bulk.d.ts.map +1 -0
  117. package/dist/lib/crud/use-entity-bulk.js +106 -0
  118. package/dist/lib/crud/use-entity-bulk.js.map +1 -0
  119. package/dist/lib/crud/use-entity-link-sources.d.ts +25 -0
  120. package/dist/lib/crud/use-entity-link-sources.d.ts.map +1 -0
  121. package/dist/lib/crud/use-entity-link-sources.js +37 -0
  122. package/dist/lib/crud/use-entity-link-sources.js.map +1 -0
  123. package/dist/lib/health/health-routes.d.ts +37 -0
  124. package/dist/lib/health/health-routes.d.ts.map +1 -0
  125. package/dist/lib/health/health-routes.js +90 -0
  126. package/dist/lib/health/health-routes.js.map +1 -0
  127. package/dist/lib/i18n/catalog/en.d.ts +4 -0
  128. package/dist/lib/i18n/catalog/en.d.ts.map +1 -0
  129. package/dist/lib/i18n/catalog/en.js +347 -0
  130. package/dist/lib/i18n/catalog/en.js.map +1 -0
  131. package/dist/lib/i18n/catalog/es.d.ts +397 -0
  132. package/dist/lib/i18n/catalog/es.d.ts.map +1 -0
  133. package/dist/lib/i18n/catalog/es.js +407 -0
  134. package/dist/lib/i18n/catalog/es.js.map +1 -0
  135. package/dist/lib/i18n/catalog/index.d.ts +671 -0
  136. package/dist/lib/i18n/catalog/index.d.ts.map +1 -0
  137. package/dist/lib/i18n/catalog/index.js +28 -0
  138. package/dist/lib/i18n/catalog/index.js.map +1 -0
  139. package/dist/lib/i18n/index.d.ts +2 -0
  140. package/dist/lib/i18n/index.d.ts.map +1 -0
  141. package/dist/lib/i18n/index.js +3 -0
  142. package/dist/lib/i18n/index.js.map +1 -0
  143. package/dist/lib/i18n/locale-link.d.ts +23 -0
  144. package/dist/lib/i18n/locale-link.d.ts.map +1 -0
  145. package/dist/lib/i18n/locale-link.js +43 -0
  146. package/dist/lib/i18n/locale-link.js.map +1 -0
  147. package/dist/lib/i18n/spec-catalog.d.ts +2 -0
  148. package/dist/lib/i18n/spec-catalog.d.ts.map +1 -0
  149. package/dist/lib/i18n/spec-catalog.js +84 -0
  150. package/dist/lib/i18n/spec-catalog.js.map +1 -0
  151. package/dist/lib/search/record-search-route.d.ts +44 -0
  152. package/dist/lib/search/record-search-route.d.ts.map +1 -0
  153. package/dist/lib/search/record-search-route.js +158 -0
  154. package/dist/lib/search/record-search-route.js.map +1 -0
  155. package/dist/lib/search/record-search-source.d.ts +59 -0
  156. package/dist/lib/search/record-search-source.d.ts.map +1 -0
  157. package/dist/lib/search/record-search-source.js +118 -0
  158. package/dist/lib/search/record-search-source.js.map +1 -0
  159. package/dist/lib/search/record-search.types.d.ts +85 -0
  160. package/dist/lib/search/record-search.types.d.ts.map +1 -0
  161. package/dist/lib/search/record-search.types.js +29 -0
  162. package/dist/lib/search/record-search.types.js.map +1 -0
  163. package/dist/lib/search/search-records.d.ts +25 -0
  164. package/dist/lib/search/search-records.d.ts.map +1 -0
  165. package/dist/lib/search/search-records.js +36 -0
  166. package/dist/lib/search/search-records.js.map +1 -0
  167. package/dist/lib/session/account-links.d.ts +46 -0
  168. package/dist/lib/session/account-links.d.ts.map +1 -0
  169. package/dist/lib/session/account-links.js +41 -0
  170. package/dist/lib/session/account-links.js.map +1 -0
  171. package/dist/lib/session/bearer.d.ts +25 -0
  172. package/dist/lib/session/bearer.d.ts.map +1 -0
  173. package/dist/lib/session/bearer.js +31 -0
  174. package/dist/lib/session/bearer.js.map +1 -0
  175. package/dist/lib/session/cookies.d.ts +14 -0
  176. package/dist/lib/session/cookies.d.ts.map +1 -0
  177. package/dist/lib/session/cookies.js +33 -0
  178. package/dist/lib/session/cookies.js.map +1 -0
  179. package/dist/lib/session/device.d.ts +31 -0
  180. package/dist/lib/session/device.d.ts.map +1 -0
  181. package/dist/lib/session/device.js +80 -0
  182. package/dist/lib/session/device.js.map +1 -0
  183. package/dist/lib/session/index.d.ts +3 -0
  184. package/dist/lib/session/index.d.ts.map +1 -0
  185. package/dist/lib/session/index.js +12 -0
  186. package/dist/lib/session/index.js.map +1 -0
  187. package/dist/lib/session/refresh-route.d.ts +19 -0
  188. package/dist/lib/session/refresh-route.d.ts.map +1 -0
  189. package/dist/lib/session/refresh-route.js +85 -0
  190. package/dist/lib/session/refresh-route.js.map +1 -0
  191. package/dist/lib/session/session-keepalive.d.ts +52 -0
  192. package/dist/lib/session/session-keepalive.d.ts.map +1 -0
  193. package/dist/lib/session/session-keepalive.js +72 -0
  194. package/dist/lib/session/session-keepalive.js.map +1 -0
  195. package/dist/lib/session/use-session-refresh.d.ts +43 -0
  196. package/dist/lib/session/use-session-refresh.d.ts.map +1 -0
  197. package/dist/lib/session/use-session-refresh.js +120 -0
  198. package/dist/lib/session/use-session-refresh.js.map +1 -0
  199. package/dist/lib/wizard/index.d.ts +4 -0
  200. package/dist/lib/wizard/index.d.ts.map +1 -0
  201. package/dist/lib/wizard/index.js +5 -0
  202. package/dist/lib/wizard/index.js.map +1 -0
  203. package/dist/lib/wizard/use-wizard-draft.d.ts +22 -0
  204. package/dist/lib/wizard/use-wizard-draft.d.ts.map +1 -0
  205. package/dist/lib/wizard/use-wizard-draft.js +39 -0
  206. package/dist/lib/wizard/use-wizard-draft.js.map +1 -0
  207. package/dist/lib/wizard/use-wizard-step-param.d.ts +60 -0
  208. package/dist/lib/wizard/use-wizard-step-param.d.ts.map +1 -0
  209. package/dist/lib/wizard/use-wizard-step-param.js +94 -0
  210. package/dist/lib/wizard/use-wizard-step-param.js.map +1 -0
  211. package/dist/lib/wizard/wizard-tab-kind.d.ts +31 -0
  212. package/dist/lib/wizard/wizard-tab-kind.d.ts.map +1 -0
  213. package/dist/lib/wizard/wizard-tab-kind.js +33 -0
  214. package/dist/lib/wizard/wizard-tab-kind.js.map +1 -0
  215. package/dist/lib/workspace/drafts-state.d.ts +88 -0
  216. package/dist/lib/workspace/drafts-state.d.ts.map +1 -0
  217. package/dist/lib/workspace/drafts-state.js +128 -0
  218. package/dist/lib/workspace/drafts-state.js.map +1 -0
  219. package/dist/lib/workspace/entity-nav.d.ts +26 -0
  220. package/dist/lib/workspace/entity-nav.d.ts.map +1 -0
  221. package/dist/lib/workspace/entity-nav.js +56 -0
  222. package/dist/lib/workspace/entity-nav.js.map +1 -0
  223. package/dist/lib/workspace/entity-tab-kind.d.ts +51 -0
  224. package/dist/lib/workspace/entity-tab-kind.d.ts.map +1 -0
  225. package/dist/lib/workspace/entity-tab-kind.js +47 -0
  226. package/dist/lib/workspace/entity-tab-kind.js.map +1 -0
  227. package/dist/lib/workspace/idb-state-storage.d.ts +9 -0
  228. package/dist/lib/workspace/idb-state-storage.d.ts.map +1 -0
  229. package/dist/lib/workspace/idb-state-storage.js +32 -0
  230. package/dist/lib/workspace/idb-state-storage.js.map +1 -0
  231. package/dist/lib/workspace/index.d.ts +13 -0
  232. package/dist/lib/workspace/index.d.ts.map +1 -0
  233. package/dist/lib/workspace/index.js +14 -0
  234. package/dist/lib/workspace/index.js.map +1 -0
  235. package/dist/lib/workspace/pending-state.d.ts +84 -0
  236. package/dist/lib/workspace/pending-state.d.ts.map +1 -0
  237. package/dist/lib/workspace/pending-state.js +155 -0
  238. package/dist/lib/workspace/pending-state.js.map +1 -0
  239. package/dist/lib/workspace/pending-transactions.d.ts +44 -0
  240. package/dist/lib/workspace/pending-transactions.d.ts.map +1 -0
  241. package/dist/lib/workspace/pending-transactions.js +114 -0
  242. package/dist/lib/workspace/pending-transactions.js.map +1 -0
  243. package/dist/lib/workspace/tab-kind.d.ts +48 -0
  244. package/dist/lib/workspace/tab-kind.d.ts.map +1 -0
  245. package/dist/lib/workspace/tab-kind.js +47 -0
  246. package/dist/lib/workspace/tab-kind.js.map +1 -0
  247. package/dist/lib/workspace/tab-state.d.ts +36 -0
  248. package/dist/lib/workspace/tab-state.d.ts.map +1 -0
  249. package/dist/lib/workspace/tab-state.js +81 -0
  250. package/dist/lib/workspace/tab-state.js.map +1 -0
  251. package/dist/lib/workspace/tabs-state.d.ts +46 -0
  252. package/dist/lib/workspace/tabs-state.d.ts.map +1 -0
  253. package/dist/lib/workspace/tabs-state.js +56 -0
  254. package/dist/lib/workspace/tabs-state.js.map +1 -0
  255. package/dist/lib/workspace/use-entity-draft.d.ts +25 -0
  256. package/dist/lib/workspace/use-entity-draft.d.ts.map +1 -0
  257. package/dist/lib/workspace/use-entity-draft.js +38 -0
  258. package/dist/lib/workspace/use-entity-draft.js.map +1 -0
  259. package/dist/lib/workspace/workspace-scope.d.ts +51 -0
  260. package/dist/lib/workspace/workspace-scope.d.ts.map +1 -0
  261. package/dist/lib/workspace/workspace-scope.js +42 -0
  262. package/dist/lib/workspace/workspace-scope.js.map +1 -0
  263. package/dist/lib/workspace/workspace-shell.d.ts +33 -0
  264. package/dist/lib/workspace/workspace-shell.d.ts.map +1 -0
  265. package/dist/lib/workspace/workspace-shell.js +206 -0
  266. package/dist/lib/workspace/workspace-shell.js.map +1 -0
  267. package/dist/server.d.ts +13 -0
  268. package/dist/server.d.ts.map +1 -0
  269. package/dist/server.js +41 -0
  270. package/dist/server.js.map +1 -0
  271. package/package.json +95 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/search/record-search-source.ts"],"sourcesContent":["import {\n describeEntityColumns,\n EntifixLogicError,\n type Entity,\n type EntityConstructor,\n type EntityFieldDescriptor,\n type EntityLoadRequest,\n envelopeEntityName,\n readEntityPageEnvelope,\n serializeLoadRequestParams,\n} from '@entifix/core';\nimport { Effect, Exit } from 'effect';\n\nimport type { RecordSearchOption } from './record-search.types.js';\n\n/**\n * What a host declares in order to make one entity searchable from the palette\n * (ADR 0040).\n *\n * The search and label members are **declared**, not derived, and that is not an\n * oversight in the metadata: `linkSearchProperty`/`linkLabelProperty` are\n * properties of a *referring* accessor — they say how the owner of a relation\n * looks up its target — and an entity has no member that says \"this is what I am\n * called\". So the host that mounts the screens says it, next to the route those\n * screens live at.\n */\nexport interface RecordSearchSourceConfig<TEntity extends Entity> {\n /** Stable id, unique per host. Defaults to the entity's wire name. */\n readonly key?: string;\n readonly entityConstructor: EntityConstructor<TEntity>;\n /** Where the owning service listens, **without** the `/api` suffix. */\n readonly baseUrl: string;\n /** The `filterable` string member a term is matched against with `like`. */\n readonly searchProperty: string;\n /** The `sortable` member read as a record's label. */\n readonly labelProperty: string;\n /** An optional secondary line — a code, a role. */\n readonly sublabelProperty?: string;\n /** The group heading, as the entity's own `pluralKey`. */\n readonly labelKey: string;\n /** Where selecting a record lands. Locale-free; the caller prefixes it. */\n readonly href: (id: string) => string;\n}\n\n/** A declared source, reduced to what the fan-out actually calls. */\nexport interface RecordSearchSource {\n readonly key: string;\n readonly entity: string;\n readonly labelKey: string;\n /** The upstream URL for one term. */\n readonly url: (term: string, limit: number) => string;\n /** `undefined` when the body is not a readable `entityPage` envelope. */\n readonly read: (\n body: unknown,\n ) => { items: RecordSearchOption[]; total: number } | undefined;\n}\n\nconst descriptorOf = (\n columns: EntityFieldDescriptor[],\n name: string,\n): EntityFieldDescriptor | undefined =>\n columns.find(column => column.name === name);\n\nconst rejection = (\n entityName: string,\n member: string,\n because: string,\n fix: string,\n): EntifixLogicError =>\n new EntifixLogicError(\n `Cannot search ${entityName} by \"${member}\": ${because} ${fix}`,\n undefined,\n { entity: entityName, member },\n );\n\n/**\n * Declare a record search source, validating it against the entity's own\n * metadata **now** rather than on the first keystroke.\n *\n * Every check here guards a failure that is otherwise silent at both ends: the\n * service answers `400`, and a caller renders that as a group it could not\n * reach — which reads as \"there are no products\". `assertSearchable` took the\n * same posture for the relation picker, for the same reason.\n *\n * ⚠️ This throws at **module load**, so a bad declaration fails the whole app at\n * boot rather than just search. That is deliberate — a source silently missing\n * from a palette is indistinguishable from a permission the caller lacks — but\n * the blast radius is wider than the picker's, which fails only its own render.\n */\nexport const defineRecordSearchSource = <TEntity extends Entity>({\n key,\n entityConstructor,\n baseUrl,\n searchProperty,\n labelProperty,\n sublabelProperty,\n labelKey,\n href,\n}: RecordSearchSourceConfig<TEntity>): RecordSearchSource => {\n const entity = envelopeEntityName(entityConstructor);\n const entityName = entityConstructor.name;\n const columns = describeEntityColumns(entityConstructor);\n\n const search = descriptorOf(columns, searchProperty);\n if (search === undefined) {\n throw rejection(\n entityName,\n searchProperty,\n 'the entity declares no such member.',\n 'Name a member the entity actually has.',\n );\n }\n // `filterable` is simultaneously the server-side RSQL allowlist.\n if (!search.filterable) {\n throw rejection(\n entityName,\n searchProperty,\n 'the member is not filterable, so the service would reject the query.',\n 'Declare it `filterable` on the entity, or search another member.',\n );\n }\n // A `like` against anything else reaches `coerceValue`, which rejects a\n // partial term — so an enum member reads as declarable and is permanently\n // broken: every keystroke short of a whole value answers `400`.\n if (search.type !== 'string') {\n throw rejection(\n entityName,\n searchProperty,\n `the member is a ${search.type}, and a partial term is not a valid value for it.`,\n 'Search a string member instead.',\n );\n }\n\n const label = descriptorOf(columns, labelProperty);\n if (label === undefined) {\n throw rejection(\n entityName,\n labelProperty,\n 'the entity declares no such label member.',\n 'Name a member the entity actually has.',\n );\n }\n // The group is sorted by the label, so the flag is load-bearing: without it\n // the service rejects the sort and the group degrades whole. Unsorted was the\n // alternative and is worse — the services apply no default order, so results\n // would arrive in storage order and shuffle as records are rewritten.\n if (!label.sortable) {\n throw rejection(\n entityName,\n labelProperty,\n 'the label member is not sortable, so the service would reject the sort.',\n 'Declare it `sortable` on the entity, or label records by another member.',\n );\n }\n\n let sublabel: EntityFieldDescriptor | undefined;\n if (sublabelProperty !== undefined) {\n sublabel = descriptorOf(columns, sublabelProperty);\n if (sublabel === undefined) {\n throw rejection(\n entityName,\n sublabelProperty,\n 'the entity declares no such sublabel member.',\n 'Name a member the entity actually has, or drop `sublabelProperty`.',\n );\n }\n }\n\n const read = (\n body: unknown,\n ): { items: RecordSearchOption[]; total: number } | undefined => {\n const page = Effect.runSyncExit(\n readEntityPageEnvelope(entityConstructor, body),\n );\n if (Exit.isFailure(page)) return undefined;\n\n const items = page.value.items\n .filter(record => (record as Entity).id != null)\n .map(record => {\n const row = record as unknown as Record<string, unknown>;\n const raw = row[labelProperty];\n const id = String((record as Entity).id);\n return {\n id,\n // A blank label is as useless as a missing one, so both fall back to\n // the id — an unnamed record must stay distinguishable from the one\n // above it. Same rule the relation picker's `labelOf` applies.\n label: raw == null || raw === '' ? id : String(raw),\n ...(sublabel !== undefined && row[sublabel.name] != null\n ? { sublabel: String(row[sublabel.name]) }\n : {}),\n entity,\n href: href(id),\n } satisfies RecordSearchOption;\n });\n\n return { items, total: page.value.total };\n };\n\n return {\n key: key ?? entity,\n entity,\n labelKey,\n /**\n * Built through `serializeLoadRequestParams`, never by hand. That is\n * correctness rather than tidiness: `encodeRsqlValue` quotes a term carrying\n * any of `;,()'\"=!<>\\` or whitespace, so `Acme, Inc.` stays one value\n * instead of splitting the expression into two comparisons.\n */\n url: (term, limit) => {\n const request = {\n filtering: [\n {\n property: search.key,\n operator: 'like' as const,\n value: term,\n },\n ],\n sorting: [{ 0: { property: label.key, type: 'asc' as const } }],\n page: 1,\n pageSize: limit,\n } as unknown as EntityLoadRequest;\n\n return `${baseUrl}/api/${entity}?${serializeLoadRequestParams(request).toString()}`;\n },\n read,\n };\n};\n"],"names":["describeEntityColumns","EntifixLogicError","envelopeEntityName","readEntityPageEnvelope","serializeLoadRequestParams","Effect","Exit","descriptorOf","columns","name","find","column","rejection","entityName","member","because","fix","undefined","entity","defineRecordSearchSource","key","entityConstructor","baseUrl","searchProperty","labelProperty","sublabelProperty","labelKey","href","search","filterable","type","label","sortable","sublabel","read","body","page","runSyncExit","isFailure","items","value","filter","record","id","map","row","raw","String","total","url","term","limit","request","filtering","property","operator","sorting","pageSize","toString"],"mappings":";AAAA,SACEA,qBAAqB,EACrBC,iBAAiB,EAKjBC,kBAAkB,EAClBC,sBAAsB,EACtBC,0BAA0B,QACrB,gBAAgB;AACvB,SAASC,MAAM,EAAEC,IAAI,QAAQ,SAAS;AA8CtC,MAAMC,eAAe,CACnBC,SACAC,OAEAD,QAAQE,IAAI,CAACC,CAAAA,SAAUA,OAAOF,IAAI,KAAKA;AAEzC,MAAMG,YAAY,CAChBC,YACAC,QACAC,SACAC,MAEA,IAAIf,kBACF,CAAC,cAAc,EAAEY,WAAW,KAAK,EAAEC,OAAO,GAAG,EAAEC,QAAQ,CAAC,EAAEC,KAAK,EAC/DC,WACA;QAAEC,QAAQL;QAAYC;IAAO;AAGjC;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMK,2BAA2B,CAAyB,EAC/DC,GAAG,EACHC,iBAAiB,EACjBC,OAAO,EACPC,cAAc,EACdC,aAAa,EACbC,gBAAgB,EAChBC,QAAQ,EACRC,IAAI,EAC8B;IAClC,MAAMT,SAAShB,mBAAmBmB;IAClC,MAAMR,aAAaQ,kBAAkBZ,IAAI;IACzC,MAAMD,UAAUR,sBAAsBqB;IAEtC,MAAMO,SAASrB,aAAaC,SAASe;IACrC,IAAIK,WAAWX,WAAW;QACxB,MAAML,UACJC,YACAU,gBACA,uCACA;IAEJ;IACA,iEAAiE;IACjE,IAAI,CAACK,OAAOC,UAAU,EAAE;QACtB,MAAMjB,UACJC,YACAU,gBACA,wEACA;IAEJ;IACA,wEAAwE;IACxE,0EAA0E;IAC1E,gEAAgE;IAChE,IAAIK,OAAOE,IAAI,KAAK,UAAU;QAC5B,MAAMlB,UACJC,YACAU,gBACA,CAAC,gBAAgB,EAAEK,OAAOE,IAAI,CAAC,iDAAiD,CAAC,EACjF;IAEJ;IAEA,MAAMC,QAAQxB,aAAaC,SAASgB;IACpC,IAAIO,UAAUd,WAAW;QACvB,MAAML,UACJC,YACAW,eACA,6CACA;IAEJ;IACA,4EAA4E;IAC5E,8EAA8E;IAC9E,6EAA6E;IAC7E,sEAAsE;IACtE,IAAI,CAACO,MAAMC,QAAQ,EAAE;QACnB,MAAMpB,UACJC,YACAW,eACA,2EACA;IAEJ;IAEA,IAAIS;IACJ,IAAIR,qBAAqBR,WAAW;QAClCgB,WAAW1B,aAAaC,SAASiB;QACjC,IAAIQ,aAAahB,WAAW;YAC1B,MAAML,UACJC,YACAY,kBACA,gDACA;QAEJ;IACF;IAEA,MAAMS,OAAO,CACXC;QAEA,MAAMC,OAAO/B,OAAOgC,WAAW,CAC7BlC,uBAAuBkB,mBAAmBc;QAE5C,IAAI7B,KAAKgC,SAAS,CAACF,OAAO,OAAOnB;QAEjC,MAAMsB,QAAQH,KAAKI,KAAK,CAACD,KAAK,CAC3BE,MAAM,CAACC,CAAAA,SAAU,AAACA,OAAkBC,EAAE,IAAI,MAC1CC,GAAG,CAACF,CAAAA;YACH,MAAMG,MAAMH;YACZ,MAAMI,MAAMD,GAAG,CAACrB,cAAc;YAC9B,MAAMmB,KAAKI,OAAO,AAACL,OAAkBC,EAAE;YACvC,OAAO;gBACLA;gBACA,qEAAqE;gBACrE,oEAAoE;gBACpE,+DAA+D;gBAC/DZ,OAAOe,OAAO,QAAQA,QAAQ,KAAKH,KAAKI,OAAOD;eAC3Cb,aAAahB,aAAa4B,GAAG,CAACZ,SAASxB,IAAI,CAAC,IAAI,OAChD;gBAAEwB,UAAUc,OAAOF,GAAG,CAACZ,SAASxB,IAAI,CAAC;YAAE,IACvC,CAAC;gBACLS;gBACAS,MAAMA,KAAKgB;;QAEf;QAEF,OAAO;YAAEJ;YAAOS,OAAOZ,KAAKI,KAAK,CAACQ,KAAK;QAAC;IAC1C;IAEA,OAAO;QACL5B,GAAG,EAAEA,cAAAA,MAAOF;QACZA;QACAQ;QACA;;;;;KAKC,GACDuB,KAAK,CAACC,MAAMC;YACV,MAAMC,UAAU;gBACdC,WAAW;oBACT;wBACEC,UAAU1B,OAAOR,GAAG;wBACpBmC,UAAU;wBACVf,OAAOU;oBACT;iBACD;gBACDM,SAAS;oBAAC;wBAAE,GAAG;4BAAEF,UAAUvB,MAAMX,GAAG;4BAAEU,MAAM;wBAAe;oBAAE;iBAAE;gBAC/DM,MAAM;gBACNqB,UAAUN;YACZ;YAEA,OAAO,GAAG7B,QAAQ,KAAK,EAAEJ,OAAO,CAAC,EAAEd,2BAA2BgD,SAASM,QAAQ,IAAI;QACrF;QACAxB;IACF;AACF,EAAE"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The wire contract of `GET /api/search` — the cross-domain record search the
3
+ * command palette reads (ADR 0040).
4
+ *
5
+ * Types only, so both halves can name them: the route handler builds these on
6
+ * the server, and the browser's `searchRecords` returns them. A type crosses the
7
+ * client/server entry split freely because it erases.
8
+ */
9
+ /**
10
+ * Why a source produced no group.
11
+ *
12
+ * The split is the point, and it is not cosmetic. `forbidden` and
13
+ * `noActiveOrganization` are the **normal** state for some callers on some
14
+ * sources — an operator holds no membership, so every tenant-plane search
15
+ * answers `409` — and rendering "we could not reach this" for the ordinary case
16
+ * teaches an operator to ignore the warning. `timeout`/`network`/`unexpected`
17
+ * are the ones that mean something is actually wrong.
18
+ *
19
+ * Every member is a key in the shared `errors` catalog, so a caller renders one
20
+ * through the documented runtime-key hatch rather than inventing copy.
21
+ */
22
+ export type RecordSearchUnavailableReason = 'timeout' | 'network' | 'unauthenticated' | 'forbidden' | 'noActiveOrganization' | 'invalidQuery' | 'notFound' | 'unexpected';
23
+ /** One record a caller can show and route to. */
24
+ export interface RecordSearchOption {
25
+ readonly id: string;
26
+ readonly label: string;
27
+ /** A secondary line — a code, a role. Absent when the source declared none. */
28
+ readonly sublabel?: string;
29
+ /**
30
+ * `envelopeEntityName(Ctor)` — `key ?? name`.
31
+ *
32
+ * Repeated on every option rather than left to the group, because an option is
33
+ * routed on its own once a caller has flattened the list: `['entity', entity]`
34
+ * is exactly `entityQueryScope`'s output, and `entity:<entity>:<id>` is the
35
+ * workspace tab address.
36
+ */
37
+ readonly entity: string;
38
+ /**
39
+ * Where selecting this record lands, **without** a locale prefix — the caller
40
+ * adds one, the same way every other internal href in the back office works.
41
+ */
42
+ readonly href: string;
43
+ }
44
+ /** One source's answer. */
45
+ export interface RecordSearchGroup {
46
+ /** The source's declared key. Stable, and the caller's ordering. */
47
+ readonly source: string;
48
+ readonly entity: string;
49
+ /**
50
+ * The heading, as the entity's own `pluralKey`. A key rather than copy,
51
+ * because the server has no locale — and the entity catalogs already carry one
52
+ * for every source, so record search introduces no copy of its own.
53
+ */
54
+ readonly labelKey: string;
55
+ readonly items: readonly RecordSearchOption[];
56
+ /**
57
+ * Matches **before** the per-group limit, so "3 more" is renderable. A
58
+ * reachable source with no matches is a present group with an empty `items`,
59
+ * never an omission — that distinction is the whole reason
60
+ * {@link RecordSearchResponse.unavailable} exists.
61
+ */
62
+ readonly total: number;
63
+ }
64
+ /** One source that produced nothing, and why. */
65
+ export interface RecordSearchUnavailable {
66
+ readonly source: string;
67
+ readonly entity: string;
68
+ readonly reason: RecordSearchUnavailableReason;
69
+ /** The upstream status, when there was a response. Absent on a throw. */
70
+ readonly status?: number;
71
+ }
72
+ /**
73
+ * The whole answer.
74
+ *
75
+ * A degraded source is **named**, never a silently missing group. An empty group
76
+ * is a confident claim that there are no matching records, and a search that
77
+ * confidently reports nothing when it in fact could not look is the failure
78
+ * mode this shape exists to prevent.
79
+ */
80
+ export interface RecordSearchResponse {
81
+ readonly term: string;
82
+ readonly groups: readonly RecordSearchGroup[];
83
+ readonly unavailable: readonly RecordSearchUnavailable[];
84
+ }
85
+ //# sourceMappingURL=record-search.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record-search.types.d.ts","sourceRoot":"","sources":["../../../src/lib/search/record-search.types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,6BAA6B,GACrC,SAAS,GACT,SAAS,GACT,iBAAiB,GACjB,WAAW,GACX,sBAAsB,GACtB,cAAc,GACd,UAAU,GACV,YAAY,CAAC;AAEjB,iDAAiD;AACjD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,2BAA2B;AAC3B,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAC9C;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,iDAAiD;AACjD,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,6BAA6B,CAAC;IAC/C,yEAAyE;IACzE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC9C,QAAQ,CAAC,WAAW,EAAE,SAAS,uBAAuB,EAAE,CAAC;CAC1D"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The wire contract of `GET /api/search` — the cross-domain record search the
3
+ * command palette reads (ADR 0040).
4
+ *
5
+ * Types only, so both halves can name them: the route handler builds these on
6
+ * the server, and the browser's `searchRecords` returns them. A type crosses the
7
+ * client/server entry split freely because it erases.
8
+ */ /**
9
+ * Why a source produced no group.
10
+ *
11
+ * The split is the point, and it is not cosmetic. `forbidden` and
12
+ * `noActiveOrganization` are the **normal** state for some callers on some
13
+ * sources — an operator holds no membership, so every tenant-plane search
14
+ * answers `409` — and rendering "we could not reach this" for the ordinary case
15
+ * teaches an operator to ignore the warning. `timeout`/`network`/`unexpected`
16
+ * are the ones that mean something is actually wrong.
17
+ *
18
+ * Every member is a key in the shared `errors` catalog, so a caller renders one
19
+ * through the documented runtime-key hatch rather than inventing copy.
20
+ */ /**
21
+ * The whole answer.
22
+ *
23
+ * A degraded source is **named**, never a silently missing group. An empty group
24
+ * is a confident claim that there are no matching records, and a search that
25
+ * confidently reports nothing when it in fact could not look is the failure
26
+ * mode this shape exists to prevent.
27
+ */ export { };
28
+
29
+ //# sourceMappingURL=record-search.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/search/record-search.types.ts"],"sourcesContent":["/**\n * The wire contract of `GET /api/search` — the cross-domain record search the\n * command palette reads (ADR 0040).\n *\n * Types only, so both halves can name them: the route handler builds these on\n * the server, and the browser's `searchRecords` returns them. A type crosses the\n * client/server entry split freely because it erases.\n */\n\n/**\n * Why a source produced no group.\n *\n * The split is the point, and it is not cosmetic. `forbidden` and\n * `noActiveOrganization` are the **normal** state for some callers on some\n * sources — an operator holds no membership, so every tenant-plane search\n * answers `409` — and rendering \"we could not reach this\" for the ordinary case\n * teaches an operator to ignore the warning. `timeout`/`network`/`unexpected`\n * are the ones that mean something is actually wrong.\n *\n * Every member is a key in the shared `errors` catalog, so a caller renders one\n * through the documented runtime-key hatch rather than inventing copy.\n */\nexport type RecordSearchUnavailableReason =\n | 'timeout'\n | 'network'\n | 'unauthenticated'\n | 'forbidden'\n | 'noActiveOrganization'\n | 'invalidQuery'\n | 'notFound'\n | 'unexpected';\n\n/** One record a caller can show and route to. */\nexport interface RecordSearchOption {\n readonly id: string;\n readonly label: string;\n /** A secondary line — a code, a role. Absent when the source declared none. */\n readonly sublabel?: string;\n /**\n * `envelopeEntityName(Ctor)` — `key ?? name`.\n *\n * Repeated on every option rather than left to the group, because an option is\n * routed on its own once a caller has flattened the list: `['entity', entity]`\n * is exactly `entityQueryScope`'s output, and `entity:<entity>:<id>` is the\n * workspace tab address.\n */\n readonly entity: string;\n /**\n * Where selecting this record lands, **without** a locale prefix — the caller\n * adds one, the same way every other internal href in the back office works.\n */\n readonly href: string;\n}\n\n/** One source's answer. */\nexport interface RecordSearchGroup {\n /** The source's declared key. Stable, and the caller's ordering. */\n readonly source: string;\n readonly entity: string;\n /**\n * The heading, as the entity's own `pluralKey`. A key rather than copy,\n * because the server has no locale — and the entity catalogs already carry one\n * for every source, so record search introduces no copy of its own.\n */\n readonly labelKey: string;\n readonly items: readonly RecordSearchOption[];\n /**\n * Matches **before** the per-group limit, so \"3 more\" is renderable. A\n * reachable source with no matches is a present group with an empty `items`,\n * never an omission — that distinction is the whole reason\n * {@link RecordSearchResponse.unavailable} exists.\n */\n readonly total: number;\n}\n\n/** One source that produced nothing, and why. */\nexport interface RecordSearchUnavailable {\n readonly source: string;\n readonly entity: string;\n readonly reason: RecordSearchUnavailableReason;\n /** The upstream status, when there was a response. Absent on a throw. */\n readonly status?: number;\n}\n\n/**\n * The whole answer.\n *\n * A degraded source is **named**, never a silently missing group. An empty group\n * is a confident claim that there are no matching records, and a search that\n * confidently reports nothing when it in fact could not look is the failure\n * mode this shape exists to prevent.\n */\nexport interface RecordSearchResponse {\n readonly term: string;\n readonly groups: readonly RecordSearchGroup[];\n readonly unavailable: readonly RecordSearchUnavailable[];\n}\n"],"names":[],"mappings":"AAAA;;;;;;;CAOC,GAED;;;;;;;;;;;;CAYC,GA+DD;;;;;;;CAOC,GACD,WAIC"}
@@ -0,0 +1,25 @@
1
+ import type { RecordSearchResponse } from './record-search.types.js';
2
+ /** Where the aggregating route is mounted. Same-origin, so the cookie rides along. */
3
+ export declare const RECORD_SEARCH_PATH = "/api/search";
4
+ export interface SearchRecordsOptions {
5
+ /** Aborts an in-flight request when the term moves on. */
6
+ readonly signal?: AbortSignal;
7
+ /** Narrow the fan-out to these source keys. Omit to ask every source. */
8
+ readonly sources?: readonly string[];
9
+ /** Records per group. */
10
+ readonly limit?: number;
11
+ }
12
+ /**
13
+ * Ask the back office for records matching `term`.
14
+ *
15
+ * Deliberately framework-free — no React, no router, no debounce, no retry. It
16
+ * is a typed `fetch`, so the palette can own its own keystroke policy (which is
17
+ * where debounce and abort belong) and a server component or a test can call the
18
+ * same function.
19
+ *
20
+ * Same-origin on purpose: `entifix_at` is httpOnly and `sameSite: 'lax'`, so a
21
+ * cross-origin call would carry no session and every group would come back
22
+ * unavailable.
23
+ */
24
+ export declare const searchRecords: (term: string, { signal, sources, limit }?: SearchRecordsOptions) => Promise<RecordSearchResponse>;
25
+ //# sourceMappingURL=search-records.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-records.d.ts","sourceRoot":"","sources":["../../../src/lib/search/search-records.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAErE,sFAAsF;AACtF,eAAO,MAAM,kBAAkB,gBAAgB,CAAC;AAEhD,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,yEAAyE;IACzE,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,yBAAyB;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,MAAM,EACZ,6BAA4B,oBAAyB,KACpD,OAAO,CAAC,oBAAoB,CAoB9B,CAAC"}
@@ -0,0 +1,36 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /** Where the aggregating route is mounted. Same-origin, so the cookie rides along. */ export const RECORD_SEARCH_PATH = '/api/search';
3
+ /**
4
+ * Ask the back office for records matching `term`.
5
+ *
6
+ * Deliberately framework-free — no React, no router, no debounce, no retry. It
7
+ * is a typed `fetch`, so the palette can own its own keystroke policy (which is
8
+ * where debounce and abort belong) and a server component or a test can call the
9
+ * same function.
10
+ *
11
+ * Same-origin on purpose: `entifix_at` is httpOnly and `sameSite: 'lax'`, so a
12
+ * cross-origin call would carry no session and every group would come back
13
+ * unavailable.
14
+ */ export const searchRecords = async (term, { signal, sources, limit } = {})=>{
15
+ const params = new URLSearchParams({
16
+ q: term
17
+ });
18
+ if (sources !== undefined && sources.length > 0) {
19
+ params.set('sources', sources.join(','));
20
+ }
21
+ if (limit !== undefined) params.set('limit', String(limit));
22
+ const response = await fetch(`${RECORD_SEARCH_PATH}?${params.toString()}`, _extends({
23
+ cache: 'no-store'
24
+ }, signal === undefined ? {} : {
25
+ signal
26
+ }));
27
+ if (!response.ok) {
28
+ // The route answers `401` for a lapsed session and `400` for a caller naming
29
+ // a source that does not exist. Neither is a per-source degradation — those
30
+ // arrive inside a `200` — so both are the caller's to handle.
31
+ throw new Error(`record search failed with ${response.status}`);
32
+ }
33
+ return await response.json();
34
+ };
35
+
36
+ //# sourceMappingURL=search-records.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/search/search-records.ts"],"sourcesContent":["import type { RecordSearchResponse } from './record-search.types.js';\n\n/** Where the aggregating route is mounted. Same-origin, so the cookie rides along. */\nexport const RECORD_SEARCH_PATH = '/api/search';\n\nexport interface SearchRecordsOptions {\n /** Aborts an in-flight request when the term moves on. */\n readonly signal?: AbortSignal;\n /** Narrow the fan-out to these source keys. Omit to ask every source. */\n readonly sources?: readonly string[];\n /** Records per group. */\n readonly limit?: number;\n}\n\n/**\n * Ask the back office for records matching `term`.\n *\n * Deliberately framework-free — no React, no router, no debounce, no retry. It\n * is a typed `fetch`, so the palette can own its own keystroke policy (which is\n * where debounce and abort belong) and a server component or a test can call the\n * same function.\n *\n * Same-origin on purpose: `entifix_at` is httpOnly and `sameSite: 'lax'`, so a\n * cross-origin call would carry no session and every group would come back\n * unavailable.\n */\nexport const searchRecords = async (\n term: string,\n { signal, sources, limit }: SearchRecordsOptions = {},\n): Promise<RecordSearchResponse> => {\n const params = new URLSearchParams({ q: term });\n if (sources !== undefined && sources.length > 0) {\n params.set('sources', sources.join(','));\n }\n if (limit !== undefined) params.set('limit', String(limit));\n\n const response = await fetch(`${RECORD_SEARCH_PATH}?${params.toString()}`, {\n cache: 'no-store',\n ...(signal === undefined ? {} : { signal }),\n });\n\n if (!response.ok) {\n // The route answers `401` for a lapsed session and `400` for a caller naming\n // a source that does not exist. Neither is a per-source degradation — those\n // arrive inside a `200` — so both are the caller's to handle.\n throw new Error(`record search failed with ${response.status}`);\n }\n\n return (await response.json()) as RecordSearchResponse;\n};\n"],"names":["RECORD_SEARCH_PATH","searchRecords","term","signal","sources","limit","params","URLSearchParams","q","undefined","length","set","join","String","response","fetch","toString","cache","ok","Error","status","json"],"mappings":";AAEA,oFAAoF,GACpF,OAAO,MAAMA,qBAAqB,cAAc;AAWhD;;;;;;;;;;;CAWC,GACD,OAAO,MAAMC,gBAAgB,OAC3BC,MACA,EAAEC,MAAM,EAAEC,OAAO,EAAEC,KAAK,EAAwB,GAAG,CAAC,CAAC;IAErD,MAAMC,SAAS,IAAIC,gBAAgB;QAAEC,GAAGN;IAAK;IAC7C,IAAIE,YAAYK,aAAaL,QAAQM,MAAM,GAAG,GAAG;QAC/CJ,OAAOK,GAAG,CAAC,WAAWP,QAAQQ,IAAI,CAAC;IACrC;IACA,IAAIP,UAAUI,WAAWH,OAAOK,GAAG,CAAC,SAASE,OAAOR;IAEpD,MAAMS,WAAW,MAAMC,MAAM,GAAGf,mBAAmB,CAAC,EAAEM,OAAOU,QAAQ,IAAI,EAAE;QACzEC,OAAO;OACHd,WAAWM,YAAY,CAAC,IAAI;QAAEN;IAAO;IAG3C,IAAI,CAACW,SAASI,EAAE,EAAE;QAChB,6EAA6E;QAC7E,4EAA4E;QAC5E,8DAA8D;QAC9D,MAAM,IAAIC,MAAM,CAAC,0BAA0B,EAAEL,SAASM,MAAM,EAAE;IAChE;IAEA,OAAQ,MAAMN,SAASO,IAAI;AAC7B,EAAE"}
@@ -0,0 +1,46 @@
1
+ import type { Locale } from '@entifix/core';
2
+ /**
3
+ * The catalog keys naming these destinations, in the `shell` namespace.
4
+ *
5
+ * They are keys of `shell` and not of `app` because this list is shell-owned:
6
+ * the module that authors a string owns its catalog entry and resolves it. When
7
+ * they lived under `app:auth.*`, every host app had to resolve a key belonging
8
+ * to a *different* app's sub-tree just to render this menu.
9
+ *
10
+ * Typed as a union rather than `string` so `useT('shell')` still checks them —
11
+ * no `useTranslateKey` escape hatch is needed to render the menu.
12
+ */
13
+ export type AccountLabelKey = 'account.profile' | 'account.security' | 'account.sessions';
14
+ /** An account destination: a catalog key plus the path it lives at. */
15
+ export interface AccountDestination {
16
+ /** Key in the `shell` namespace, resolved by whichever control renders it. */
17
+ readonly labelKey: AccountLabelKey;
18
+ /** Path within the host serving the account surface, with no locale prefix. */
19
+ readonly path: string;
20
+ }
21
+ /**
22
+ * Every account screen, in one list.
23
+ *
24
+ * The sidebar section and the account menu are the same three destinations and
25
+ * were drifting as two hand-kept lists; both build from this one now, so adding
26
+ * a screen is a one-line change here.
27
+ *
28
+ * It lives in the shared shell rather than in the auth shell because the menu
29
+ * is rendered by `BackOfficeShell`, which any host mounts — a `shell:base`
30
+ * package cannot reach up into a `shell:domain` one.
31
+ */
32
+ export declare const ACCOUNT_DESTINATIONS: readonly AccountDestination[];
33
+ export interface AccountLink {
34
+ readonly labelKey: AccountLabelKey;
35
+ readonly href: string;
36
+ }
37
+ /**
38
+ * Account links as locale-prefixed paths on the host that serves them.
39
+ *
40
+ * There is no absolute-URL variant any more. There used to be one, for the apps
41
+ * that linked across to auth-app on another port — the account surface and the
42
+ * back office share an origin now, so every one of these is an ordinary in-app
43
+ * navigation and a cross-origin builder would have no caller.
44
+ */
45
+ export declare const accountPaths: (locale: Locale) => readonly AccountLink[];
46
+ //# sourceMappingURL=account-links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account-links.d.ts","sourceRoot":"","sources":["../../../src/lib/session/account-links.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAE5C;;;;;;;;;;GAUG;AACH,MAAM,MAAM,eAAe,GACzB,iBAAiB,GAAG,kBAAkB,GAAG,kBAAkB,CAAC;AAE9D,uEAAuE;AACvE,MAAM,WAAW,kBAAkB;IACjC,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,EAAE,SAAS,kBAAkB,EAQ7D,CAAC;AAEF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,MAAM,KAAG,SAAS,WAAW,EAI7D,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Every account screen, in one list.
3
+ *
4
+ * The sidebar section and the account menu are the same three destinations and
5
+ * were drifting as two hand-kept lists; both build from this one now, so adding
6
+ * a screen is a one-line change here.
7
+ *
8
+ * It lives in the shared shell rather than in the auth shell because the menu
9
+ * is rendered by `BackOfficeShell`, which any host mounts — a `shell:base`
10
+ * package cannot reach up into a `shell:domain` one.
11
+ */ export const ACCOUNT_DESTINATIONS = [
12
+ {
13
+ labelKey: 'account.profile',
14
+ path: '/account'
15
+ },
16
+ // Not `/account/password` any more. r10c holds no password to change, so the
17
+ // screen behind this is a set of links into the provider's own self-service —
18
+ // which is also where a second factor is enrolled and a social account is
19
+ // linked, neither of which had anywhere to live before.
20
+ {
21
+ labelKey: 'account.security',
22
+ path: '/account/security'
23
+ },
24
+ {
25
+ labelKey: 'account.sessions',
26
+ path: '/account/sessions'
27
+ }
28
+ ];
29
+ /**
30
+ * Account links as locale-prefixed paths on the host that serves them.
31
+ *
32
+ * There is no absolute-URL variant any more. There used to be one, for the apps
33
+ * that linked across to auth-app on another port — the account surface and the
34
+ * back office share an origin now, so every one of these is an ordinary in-app
35
+ * navigation and a cross-origin builder would have no caller.
36
+ */ export const accountPaths = (locale)=>ACCOUNT_DESTINATIONS.map((destination)=>({
37
+ labelKey: destination.labelKey,
38
+ href: `/${locale}${destination.path}`
39
+ }));
40
+
41
+ //# sourceMappingURL=account-links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/session/account-links.ts"],"sourcesContent":["import type { Locale } from '@entifix/core';\n\n/**\n * The catalog keys naming these destinations, in the `shell` namespace.\n *\n * They are keys of `shell` and not of `app` because this list is shell-owned:\n * the module that authors a string owns its catalog entry and resolves it. When\n * they lived under `app:auth.*`, every host app had to resolve a key belonging\n * to a *different* app's sub-tree just to render this menu.\n *\n * Typed as a union rather than `string` so `useT('shell')` still checks them —\n * no `useTranslateKey` escape hatch is needed to render the menu.\n */\nexport type AccountLabelKey =\n 'account.profile' | 'account.security' | 'account.sessions';\n\n/** An account destination: a catalog key plus the path it lives at. */\nexport interface AccountDestination {\n /** Key in the `shell` namespace, resolved by whichever control renders it. */\n readonly labelKey: AccountLabelKey;\n /** Path within the host serving the account surface, with no locale prefix. */\n readonly path: string;\n}\n\n/**\n * Every account screen, in one list.\n *\n * The sidebar section and the account menu are the same three destinations and\n * were drifting as two hand-kept lists; both build from this one now, so adding\n * a screen is a one-line change here.\n *\n * It lives in the shared shell rather than in the auth shell because the menu\n * is rendered by `BackOfficeShell`, which any host mounts — a `shell:base`\n * package cannot reach up into a `shell:domain` one.\n */\nexport const ACCOUNT_DESTINATIONS: readonly AccountDestination[] = [\n { labelKey: 'account.profile', path: '/account' },\n // Not `/account/password` any more. r10c holds no password to change, so the\n // screen behind this is a set of links into the provider's own self-service —\n // which is also where a second factor is enrolled and a social account is\n // linked, neither of which had anywhere to live before.\n { labelKey: 'account.security', path: '/account/security' },\n { labelKey: 'account.sessions', path: '/account/sessions' },\n];\n\nexport interface AccountLink {\n readonly labelKey: AccountLabelKey;\n readonly href: string;\n}\n\n/**\n * Account links as locale-prefixed paths on the host that serves them.\n *\n * There is no absolute-URL variant any more. There used to be one, for the apps\n * that linked across to auth-app on another port — the account surface and the\n * back office share an origin now, so every one of these is an ordinary in-app\n * navigation and a cross-origin builder would have no caller.\n */\nexport const accountPaths = (locale: Locale): readonly AccountLink[] =>\n ACCOUNT_DESTINATIONS.map(destination => ({\n labelKey: destination.labelKey,\n href: `/${locale}${destination.path}`,\n }));\n"],"names":["ACCOUNT_DESTINATIONS","labelKey","path","accountPaths","locale","map","destination","href"],"mappings":"AAwBA;;;;;;;;;;CAUC,GACD,OAAO,MAAMA,uBAAsD;IACjE;QAAEC,UAAU;QAAmBC,MAAM;IAAW;IAChD,6EAA6E;IAC7E,8EAA8E;IAC9E,0EAA0E;IAC1E,wDAAwD;IACxD;QAAED,UAAU;QAAoBC,MAAM;IAAoB;IAC1D;QAAED,UAAU;QAAoBC,MAAM;IAAoB;CAC3D,CAAC;AAOF;;;;;;;CAOC,GACD,OAAO,MAAMC,eAAe,CAACC,SAC3BJ,qBAAqBK,GAAG,CAACC,CAAAA,cAAgB,CAAA;YACvCL,UAAUK,YAAYL,QAAQ;YAC9BM,MAAM,CAAC,CAAC,EAAEH,SAASE,YAAYJ,IAAI,EAAE;QACvC,CAAA,GAAI"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The caller's access token, or `undefined` when they hold no session.
3
+ *
4
+ * `entifix_at` is httpOnly, so only the server can read it — which is the whole
5
+ * reason a browser talks to a backend through this app's own origin rather than
6
+ * calling `:310N` directly.
7
+ */
8
+ export declare const sessionToken: () => Promise<string | undefined>;
9
+ /**
10
+ * The token as a forwardable header, and `{}` when there is none.
11
+ *
12
+ * The empty case is the point: a header of `Bearer undefined` is not "no
13
+ * credential", it is a malformed one, and a service that answers `401` to both
14
+ * hides the difference right up until one of them starts answering something
15
+ * else. Omitting the header lets the upstream apply its own rule for an
16
+ * anonymous caller — which for `catalog-reference` reads is to allow it.
17
+ *
18
+ * This lives on its own because three server surfaces carry the same cookie the
19
+ * same way — the per-backend proxy, the auth shell's hand-written handlers, and
20
+ * the record search fan-out — and none of them grants anything by doing so. They
21
+ * only carry; the service still verifies the token and applies its own
22
+ * `requirePermission`.
23
+ */
24
+ export declare const bearerHeader: (token: string | undefined) => Record<string, string>;
25
+ //# sourceMappingURL=bearer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bearer.d.ts","sourceRoot":"","sources":["../../../src/lib/session/bearer.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,QAAa,OAAO,CAAC,MAAM,GAAG,SAAS,CACnB,CAAC;AAE9C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,YAAY,GACvB,OAAO,MAAM,GAAG,SAAS,KACxB,MAAM,CAAC,MAAM,EAAE,MAAM,CACyC,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { ACCESS_COOKIE } from '@entifix/core';
2
+ import { cookies } from 'next/headers';
3
+ /**
4
+ * The caller's access token, or `undefined` when they hold no session.
5
+ *
6
+ * `entifix_at` is httpOnly, so only the server can read it — which is the whole
7
+ * reason a browser talks to a backend through this app's own origin rather than
8
+ * calling `:310N` directly.
9
+ */ export const sessionToken = async ()=>{
10
+ var _get;
11
+ return (_get = (await cookies()).get(ACCESS_COOKIE)) == null ? void 0 : _get.value;
12
+ };
13
+ /**
14
+ * The token as a forwardable header, and `{}` when there is none.
15
+ *
16
+ * The empty case is the point: a header of `Bearer undefined` is not "no
17
+ * credential", it is a malformed one, and a service that answers `401` to both
18
+ * hides the difference right up until one of them starts answering something
19
+ * else. Omitting the header lets the upstream apply its own rule for an
20
+ * anonymous caller — which for `catalog-reference` reads is to allow it.
21
+ *
22
+ * This lives on its own because three server surfaces carry the same cookie the
23
+ * same way — the per-backend proxy, the auth shell's hand-written handlers, and
24
+ * the record search fan-out — and none of them grants anything by doing so. They
25
+ * only carry; the service still verifies the token and applies its own
26
+ * `requirePermission`.
27
+ */ export const bearerHeader = (token)=>token === undefined ? {} : {
28
+ Authorization: `Bearer ${token}`
29
+ };
30
+
31
+ //# sourceMappingURL=bearer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/session/bearer.ts"],"sourcesContent":["import { ACCESS_COOKIE } from '@entifix/core';\nimport { cookies } from 'next/headers';\n\n/**\n * The caller's access token, or `undefined` when they hold no session.\n *\n * `entifix_at` is httpOnly, so only the server can read it — which is the whole\n * reason a browser talks to a backend through this app's own origin rather than\n * calling `:310N` directly.\n */\nexport const sessionToken = async (): Promise<string | undefined> =>\n (await cookies()).get(ACCESS_COOKIE)?.value;\n\n/**\n * The token as a forwardable header, and `{}` when there is none.\n *\n * The empty case is the point: a header of `Bearer undefined` is not \"no\n * credential\", it is a malformed one, and a service that answers `401` to both\n * hides the difference right up until one of them starts answering something\n * else. Omitting the header lets the upstream apply its own rule for an\n * anonymous caller — which for `catalog-reference` reads is to allow it.\n *\n * This lives on its own because three server surfaces carry the same cookie the\n * same way — the per-backend proxy, the auth shell's hand-written handlers, and\n * the record search fan-out — and none of them grants anything by doing so. They\n * only carry; the service still verifies the token and applies its own\n * `requirePermission`.\n */\nexport const bearerHeader = (\n token: string | undefined,\n): Record<string, string> =>\n token === undefined ? {} : { Authorization: `Bearer ${token}` };\n"],"names":["ACCESS_COOKIE","cookies","sessionToken","get","value","bearerHeader","token","undefined","Authorization"],"mappings":"AAAA,SAASA,aAAa,QAAQ,gBAAgB;AAC9C,SAASC,OAAO,QAAQ,eAAe;AAEvC;;;;;;CAMC,GACD,OAAO,MAAMC,eAAe;QAC1B;YAAA,OAAA,AAAC,CAAA,MAAMD,SAAQ,EAAGE,GAAG,CAACH,mCAAtB,KAAsCI,KAAK;EAAC;AAE9C;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMC,eAAe,CAC1BC,QAEAA,UAAUC,YAAY,CAAC,IAAI;QAAEC,eAAe,CAAC,OAAO,EAAEF,OAAO;IAAC,EAAE"}
@@ -0,0 +1,14 @@
1
+ import type { NextResponse } from 'next/server';
2
+ /** What auth-service hands back from login / register / refresh. */
3
+ export interface SessionCookiePayload {
4
+ readonly accessToken: string;
5
+ /** Absent on a refresh: the session id does not change. */
6
+ readonly sessionId?: string;
7
+ /** Seconds until the session's absolute ceiling — both cookies are sized to it. */
8
+ readonly sessionExpiresIn: number;
9
+ }
10
+ /** Write the session cookies onto an outgoing response. */
11
+ export declare const applySessionCookies: (response: NextResponse, payload: SessionCookiePayload) => NextResponse;
12
+ /** Clear both cookies on an outgoing response (sign-out, dead session). */
13
+ export declare const clearSessionCookies: (response: NextResponse) => NextResponse;
14
+ //# sourceMappingURL=cookies.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../../src/lib/session/cookies.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,oEAAoE;AACpE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,2DAA2D;IAC3D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,mFAAmF;IACnF,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACnC;AAqBD,2DAA2D;AAC3D,eAAO,MAAM,mBAAmB,GAC9B,UAAU,YAAY,EACtB,SAAS,oBAAoB,KAC5B,YAeF,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,mBAAmB,GAAI,UAAU,YAAY,KAAG,YAI5D,CAAC"}
@@ -0,0 +1,33 @@
1
+ import { ACCESS_COOKIE, SESSION_COOKIE } from '@entifix/core';
2
+ /**
3
+ * Both cookies outlive the access token on purpose.
4
+ *
5
+ * Sizing `entifix_at` to the token's own 15 minutes is what used to sign everyone
6
+ * out four times an hour: once the cookie vanished, the middleware's presence
7
+ * check could no longer tell "your token needs refreshing" from "you were never
8
+ * signed in", and chose the second. The JWT's `exp` is the real authority on
9
+ * whether a token is usable; the cookie only has to survive long enough for the
10
+ * refresh path to notice.
11
+ */ const cookieOptions = (maxAge)=>({
12
+ httpOnly: true,
13
+ sameSite: 'lax',
14
+ // Host-scoped in dev (localhost shares cookies across ports); Secure in production.
15
+ secure: process.env.NODE_ENV === 'production',
16
+ path: '/',
17
+ maxAge
18
+ });
19
+ /** Write the session cookies onto an outgoing response. */ export const applySessionCookies = (response, payload)=>{
20
+ const maxAge = payload.sessionExpiresIn;
21
+ response.cookies.set(ACCESS_COOKIE, payload.accessToken, cookieOptions(maxAge));
22
+ if (payload.sessionId !== undefined) {
23
+ response.cookies.set(SESSION_COOKIE, payload.sessionId, cookieOptions(maxAge));
24
+ }
25
+ return response;
26
+ };
27
+ /** Clear both cookies on an outgoing response (sign-out, dead session). */ export const clearSessionCookies = (response)=>{
28
+ response.cookies.delete(ACCESS_COOKIE);
29
+ response.cookies.delete(SESSION_COOKIE);
30
+ return response;
31
+ };
32
+
33
+ //# sourceMappingURL=cookies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/session/cookies.ts"],"sourcesContent":["import { ACCESS_COOKIE, SESSION_COOKIE } from '@entifix/core';\nimport type { NextResponse } from 'next/server';\n\n/** What auth-service hands back from login / register / refresh. */\nexport interface SessionCookiePayload {\n readonly accessToken: string;\n /** Absent on a refresh: the session id does not change. */\n readonly sessionId?: string;\n /** Seconds until the session's absolute ceiling — both cookies are sized to it. */\n readonly sessionExpiresIn: number;\n}\n\n/**\n * Both cookies outlive the access token on purpose.\n *\n * Sizing `entifix_at` to the token's own 15 minutes is what used to sign everyone\n * out four times an hour: once the cookie vanished, the middleware's presence\n * check could no longer tell \"your token needs refreshing\" from \"you were never\n * signed in\", and chose the second. The JWT's `exp` is the real authority on\n * whether a token is usable; the cookie only has to survive long enough for the\n * refresh path to notice.\n */\nconst cookieOptions = (maxAge: number) => ({\n httpOnly: true,\n sameSite: 'lax' as const,\n // Host-scoped in dev (localhost shares cookies across ports); Secure in production.\n secure: process.env.NODE_ENV === 'production',\n path: '/',\n maxAge,\n});\n\n/** Write the session cookies onto an outgoing response. */\nexport const applySessionCookies = (\n response: NextResponse,\n payload: SessionCookiePayload,\n): NextResponse => {\n const maxAge = payload.sessionExpiresIn;\n response.cookies.set(\n ACCESS_COOKIE,\n payload.accessToken,\n cookieOptions(maxAge),\n );\n if (payload.sessionId !== undefined) {\n response.cookies.set(\n SESSION_COOKIE,\n payload.sessionId,\n cookieOptions(maxAge),\n );\n }\n return response;\n};\n\n/** Clear both cookies on an outgoing response (sign-out, dead session). */\nexport const clearSessionCookies = (response: NextResponse): NextResponse => {\n response.cookies.delete(ACCESS_COOKIE);\n response.cookies.delete(SESSION_COOKIE);\n return response;\n};\n"],"names":["ACCESS_COOKIE","SESSION_COOKIE","cookieOptions","maxAge","httpOnly","sameSite","secure","process","env","NODE_ENV","path","applySessionCookies","response","payload","sessionExpiresIn","cookies","set","accessToken","sessionId","undefined","clearSessionCookies","delete"],"mappings":"AAAA,SAASA,aAAa,EAAEC,cAAc,QAAQ,gBAAgB;AAY9D;;;;;;;;;CASC,GACD,MAAMC,gBAAgB,CAACC,SAAoB,CAAA;QACzCC,UAAU;QACVC,UAAU;QACV,oFAAoF;QACpFC,QAAQC,QAAQC,GAAG,CAACC,QAAQ,KAAK;QACjCC,MAAM;QACNP;IACF,CAAA;AAEA,yDAAyD,GACzD,OAAO,MAAMQ,sBAAsB,CACjCC,UACAC;IAEA,MAAMV,SAASU,QAAQC,gBAAgB;IACvCF,SAASG,OAAO,CAACC,GAAG,CAClBhB,eACAa,QAAQI,WAAW,EACnBf,cAAcC;IAEhB,IAAIU,QAAQK,SAAS,KAAKC,WAAW;QACnCP,SAASG,OAAO,CAACC,GAAG,CAClBf,gBACAY,QAAQK,SAAS,EACjBhB,cAAcC;IAElB;IACA,OAAOS;AACT,EAAE;AAEF,yEAAyE,GACzE,OAAO,MAAMQ,sBAAsB,CAACR;IAClCA,SAASG,OAAO,CAACM,MAAM,CAACrB;IACxBY,SAASG,OAAO,CAACM,MAAM,CAACpB;IACxB,OAAOW;AACT,EAAE"}
@@ -0,0 +1,31 @@
1
+ import type { DeviceContext } from '@entifix/business';
2
+ import { type NextRequest, type NextResponse } from 'next/server';
3
+ /**
4
+ * Reduce a client IP to its /24 (or the v6 prefix).
5
+ *
6
+ * Enough to notice "this sign-in came from somewhere else", without keeping a
7
+ * precise location against every session.
8
+ */
9
+ export declare const truncateIp: (ip: string | undefined) => string | undefined;
10
+ /**
11
+ * Read (or mint) the device context for a request.
12
+ *
13
+ * Parsing uses `userAgent()` from `next/server` — already bundled with Next, so
14
+ * no dependency is added, and notably not `ua-parser-js`, whose v2 is
15
+ * AGPL/dual-licensed.
16
+ *
17
+ * Returns `issued` when the id had to be minted, so the caller knows to write
18
+ * the cookie onto its response.
19
+ */
20
+ export declare const readDeviceContext: (request: NextRequest) => {
21
+ device: DeviceContext;
22
+ issued: boolean;
23
+ };
24
+ /**
25
+ * Persist the device id.
26
+ *
27
+ * `httpOnly` because nothing in the browser needs to read it, and `lax` so it
28
+ * still arrives on the top-level navigation back from a sign-in.
29
+ */
30
+ export declare const applyDeviceCookie: (response: NextResponse, deviceId: string) => NextResponse;
31
+ //# sourceMappingURL=device.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../../../src/lib/session/device.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAa,MAAM,aAAa,CAAC;AAmB7E;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,IAAI,MAAM,GAAG,SAAS,KAAG,MAAM,GAAG,SAO5D,CAAC;AAcF;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAC5B,SAAS,WAAW,KACnB;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAe1C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAC5B,UAAU,YAAY,EACtB,UAAU,MAAM,KACf,YASF,CAAC"}
@@ -0,0 +1,80 @@
1
+ import { DEVICE_COOKIE } from '@entifix/core';
2
+ import { userAgent } from 'next/server';
3
+ /** Two years: long enough that a familiar browser stays familiar. */ const DID_MAX_AGE = 60 * 60 * 24 * 730;
4
+ /**
5
+ * Mint an unguessable device id.
6
+ *
7
+ * `crypto.getRandomValues` rather than `node:crypto`, so this module also runs
8
+ * on the edge runtime.
9
+ */ const mintDeviceId = ()=>{
10
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
11
+ return btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
12
+ };
13
+ /**
14
+ * Reduce a client IP to its /24 (or the v6 prefix).
15
+ *
16
+ * Enough to notice "this sign-in came from somewhere else", without keeping a
17
+ * precise location against every session.
18
+ */ export const truncateIp = (ip)=>{
19
+ if (ip === undefined || ip === '') return undefined;
20
+ if (ip.includes(':')) {
21
+ return `${ip.split(':').slice(0, 4).join(':')}::`;
22
+ }
23
+ const octets = ip.split('.');
24
+ return octets.length === 4 ? `${octets.slice(0, 3).join('.')}.0` : undefined;
25
+ };
26
+ /**
27
+ * The client IP as the proxy reports it.
28
+ *
29
+ * `x-forwarded-for` is client-controlled unless a trusted proxy overwrites it,
30
+ * which is why the value is only ever a label — see {@link DeviceContext}.
31
+ */ const clientIp = (request)=>{
32
+ var _request_headers_get;
33
+ var _forwarded_split_;
34
+ const forwarded = request.headers.get('x-forwarded-for');
35
+ if (forwarded !== null) return (_forwarded_split_ = forwarded.split(',')[0]) == null ? void 0 : _forwarded_split_.trim();
36
+ return (_request_headers_get = request.headers.get('x-real-ip')) != null ? _request_headers_get : undefined;
37
+ };
38
+ /**
39
+ * Read (or mint) the device context for a request.
40
+ *
41
+ * Parsing uses `userAgent()` from `next/server` — already bundled with Next, so
42
+ * no dependency is added, and notably not `ua-parser-js`, whose v2 is
43
+ * AGPL/dual-licensed.
44
+ *
45
+ * Returns `issued` when the id had to be minted, so the caller knows to write
46
+ * the cookie onto its response.
47
+ */ export const readDeviceContext = (request)=>{
48
+ var _agent_device_type;
49
+ var _request_cookies_get;
50
+ const existing = (_request_cookies_get = request.cookies.get(DEVICE_COOKIE)) == null ? void 0 : _request_cookies_get.value;
51
+ const deviceId = existing != null ? existing : mintDeviceId();
52
+ const agent = userAgent(request);
53
+ return {
54
+ device: {
55
+ deviceId,
56
+ browser: agent.browser.name,
57
+ os: agent.os.name,
58
+ type: (_agent_device_type = agent.device.type) != null ? _agent_device_type : 'desktop',
59
+ ip: truncateIp(clientIp(request))
60
+ },
61
+ issued: existing === undefined
62
+ };
63
+ };
64
+ /**
65
+ * Persist the device id.
66
+ *
67
+ * `httpOnly` because nothing in the browser needs to read it, and `lax` so it
68
+ * still arrives on the top-level navigation back from a sign-in.
69
+ */ export const applyDeviceCookie = (response, deviceId)=>{
70
+ response.cookies.set(DEVICE_COOKIE, deviceId, {
71
+ httpOnly: true,
72
+ sameSite: 'lax',
73
+ secure: process.env.NODE_ENV === 'production',
74
+ path: '/',
75
+ maxAge: DID_MAX_AGE
76
+ });
77
+ return response;
78
+ };
79
+
80
+ //# sourceMappingURL=device.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/session/device.ts"],"sourcesContent":["import type { DeviceContext } from '@entifix/business';\nimport { DEVICE_COOKIE } from '@entifix/core';\nimport { type NextRequest, type NextResponse, userAgent } from 'next/server';\n\n/** Two years: long enough that a familiar browser stays familiar. */\nconst DID_MAX_AGE = 60 * 60 * 24 * 730;\n\n/**\n * Mint an unguessable device id.\n *\n * `crypto.getRandomValues` rather than `node:crypto`, so this module also runs\n * on the edge runtime.\n */\nconst mintDeviceId = (): string => {\n const bytes = crypto.getRandomValues(new Uint8Array(32));\n return btoa(String.fromCharCode(...bytes))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n};\n\n/**\n * Reduce a client IP to its /24 (or the v6 prefix).\n *\n * Enough to notice \"this sign-in came from somewhere else\", without keeping a\n * precise location against every session.\n */\nexport const truncateIp = (ip: string | undefined): string | undefined => {\n if (ip === undefined || ip === '') return undefined;\n if (ip.includes(':')) {\n return `${ip.split(':').slice(0, 4).join(':')}::`;\n }\n const octets = ip.split('.');\n return octets.length === 4 ? `${octets.slice(0, 3).join('.')}.0` : undefined;\n};\n\n/**\n * The client IP as the proxy reports it.\n *\n * `x-forwarded-for` is client-controlled unless a trusted proxy overwrites it,\n * which is why the value is only ever a label — see {@link DeviceContext}.\n */\nconst clientIp = (request: NextRequest): string | undefined => {\n const forwarded = request.headers.get('x-forwarded-for');\n if (forwarded !== null) return forwarded.split(',')[0]?.trim();\n return request.headers.get('x-real-ip') ?? undefined;\n};\n\n/**\n * Read (or mint) the device context for a request.\n *\n * Parsing uses `userAgent()` from `next/server` — already bundled with Next, so\n * no dependency is added, and notably not `ua-parser-js`, whose v2 is\n * AGPL/dual-licensed.\n *\n * Returns `issued` when the id had to be minted, so the caller knows to write\n * the cookie onto its response.\n */\nexport const readDeviceContext = (\n request: NextRequest,\n): { device: DeviceContext; issued: boolean } => {\n const existing = request.cookies.get(DEVICE_COOKIE)?.value;\n const deviceId = existing ?? mintDeviceId();\n const agent = userAgent(request);\n\n return {\n device: {\n deviceId,\n browser: agent.browser.name,\n os: agent.os.name,\n type: agent.device.type ?? 'desktop',\n ip: truncateIp(clientIp(request)),\n },\n issued: existing === undefined,\n };\n};\n\n/**\n * Persist the device id.\n *\n * `httpOnly` because nothing in the browser needs to read it, and `lax` so it\n * still arrives on the top-level navigation back from a sign-in.\n */\nexport const applyDeviceCookie = (\n response: NextResponse,\n deviceId: string,\n): NextResponse => {\n response.cookies.set(DEVICE_COOKIE, deviceId, {\n httpOnly: true,\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n path: '/',\n maxAge: DID_MAX_AGE,\n });\n return response;\n};\n"],"names":["DEVICE_COOKIE","userAgent","DID_MAX_AGE","mintDeviceId","bytes","crypto","getRandomValues","Uint8Array","btoa","String","fromCharCode","replace","truncateIp","ip","undefined","includes","split","slice","join","octets","length","clientIp","request","forwarded","headers","get","trim","readDeviceContext","agent","existing","cookies","value","deviceId","device","browser","name","os","type","issued","applyDeviceCookie","response","set","httpOnly","sameSite","secure","process","env","NODE_ENV","path","maxAge"],"mappings":"AACA,SAASA,aAAa,QAAQ,gBAAgB;AAC9C,SAA8CC,SAAS,QAAQ,cAAc;AAE7E,mEAAmE,GACnE,MAAMC,cAAc,KAAK,KAAK,KAAK;AAEnC;;;;;CAKC,GACD,MAAMC,eAAe;IACnB,MAAMC,QAAQC,OAAOC,eAAe,CAAC,IAAIC,WAAW;IACpD,OAAOC,KAAKC,OAAOC,YAAY,IAAIN,QAChCO,OAAO,CAAC,OAAO,KACfA,OAAO,CAAC,OAAO,KACfA,OAAO,CAAC,OAAO;AACpB;AAEA;;;;;CAKC,GACD,OAAO,MAAMC,aAAa,CAACC;IACzB,IAAIA,OAAOC,aAAaD,OAAO,IAAI,OAAOC;IAC1C,IAAID,GAAGE,QAAQ,CAAC,MAAM;QACpB,OAAO,GAAGF,GAAGG,KAAK,CAAC,KAAKC,KAAK,CAAC,GAAG,GAAGC,IAAI,CAAC,KAAK,EAAE,CAAC;IACnD;IACA,MAAMC,SAASN,GAAGG,KAAK,CAAC;IACxB,OAAOG,OAAOC,MAAM,KAAK,IAAI,GAAGD,OAAOF,KAAK,CAAC,GAAG,GAAGC,IAAI,CAAC,KAAK,EAAE,CAAC,GAAGJ;AACrE,EAAE;AAEF;;;;;CAKC,GACD,MAAMO,WAAW,CAACC;QAGTA;QADwBC;IAD/B,MAAMA,YAAYD,QAAQE,OAAO,CAACC,GAAG,CAAC;IACtC,IAAIF,cAAc,MAAM,QAAOA,oBAAAA,UAAUP,KAAK,CAAC,IAAI,CAAC,EAAE,qBAAvBO,kBAAyBG,IAAI;IAC5D,QAAOJ,uBAAAA,QAAQE,OAAO,CAACC,GAAG,CAAC,wBAApBH,uBAAoCR;AAC7C;AAEA;;;;;;;;;CASC,GACD,OAAO,MAAMa,oBAAoB,CAC/BL;QAWUM;QATON;IAAjB,MAAMO,YAAWP,uBAAAA,QAAQQ,OAAO,CAACL,GAAG,CAACzB,mCAApBsB,qBAAoCS,KAAK;IAC1D,MAAMC,WAAWH,mBAAAA,WAAY1B;IAC7B,MAAMyB,QAAQ3B,UAAUqB;IAExB,OAAO;QACLW,QAAQ;YACND;YACAE,SAASN,MAAMM,OAAO,CAACC,IAAI;YAC3BC,IAAIR,MAAMQ,EAAE,CAACD,IAAI;YACjBE,IAAI,GAAET,qBAAAA,MAAMK,MAAM,CAACI,IAAI,YAAjBT,qBAAqB;YAC3Bf,IAAID,WAAWS,SAASC;QAC1B;QACAgB,QAAQT,aAAaf;IACvB;AACF,EAAE;AAEF;;;;;CAKC,GACD,OAAO,MAAMyB,oBAAoB,CAC/BC,UACAR;IAEAQ,SAASV,OAAO,CAACW,GAAG,CAACzC,eAAegC,UAAU;QAC5CU,UAAU;QACVC,UAAU;QACVC,QAAQC,QAAQC,GAAG,CAACC,QAAQ,KAAK;QACjCC,MAAM;QACNC,QAAQ/C;IACV;IACA,OAAOsC;AACT,EAAE"}
@@ -0,0 +1,3 @@
1
+ export * from './session-keepalive.js';
2
+ export * from './use-session-refresh.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/session/index.ts"],"names":[],"mappings":"AAQA,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC"}
@@ -0,0 +1,12 @@
1
+ // Client-safe session helpers only.
2
+ //
3
+ // The cookie writers, the refresh route handler AND the account-link builders
4
+ // ship from `@entifix/next-shell/server` instead. The link builders are
5
+ // pure functions, but everything reachable from this entry is stamped
6
+ // `"use client"` by the bundler — so exporting them here turns a plain function
7
+ // call in a server layout into "Attempted to call accountPaths() from the
8
+ // server but accountPaths is on the client".
9
+ export * from './session-keepalive.js';
10
+ export * from './use-session-refresh.js';
11
+
12
+ //# sourceMappingURL=index.js.map