asma-core-helpers 0.7.2 → 0.9.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 (42) hide show
  1. package/lib/g-definitions.d.ts +13 -1
  2. package/lib/g-definitions.d.ts.map +1 -1
  3. package/lib/g-definitions.js.map +1 -1
  4. package/lib/helpers/adopusNamespaceExchange.d.ts +44 -0
  5. package/lib/helpers/adopusNamespaceExchange.d.ts.map +1 -0
  6. package/lib/helpers/adopusNamespaceExchange.js +126 -0
  7. package/lib/helpers/adopusNamespaceExchange.js.map +1 -0
  8. package/lib/helpers/generateSrvAuthBindings.d.ts +8 -8
  9. package/lib/helpers/generateSrvAuthBindings.d.ts.map +1 -1
  10. package/lib/helpers/generateSrvAuthBindings.js +6 -2
  11. package/lib/helpers/generateSrvAuthBindings.js.map +1 -1
  12. package/lib/helpers/generateSrvAuthBindings.types.d.ts +1 -1
  13. package/lib/helpers/generateSrvAuthBindings.types.d.ts.map +1 -1
  14. package/lib/helpers/getDefaultAppVersions.d.ts +27 -0
  15. package/lib/helpers/getDefaultAppVersions.d.ts.map +1 -0
  16. package/lib/helpers/getDefaultAppVersions.js +31 -0
  17. package/lib/helpers/getDefaultAppVersions.js.map +1 -0
  18. package/lib/helpers/initASMAAppVitals.d.ts.map +1 -1
  19. package/lib/helpers/initASMAAppVitals.js +11 -6
  20. package/lib/helpers/initASMAAppVitals.js.map +1 -1
  21. package/lib/helpers/isUUID.d.ts +1 -1
  22. package/lib/helpers/isUUID.d.ts.map +1 -1
  23. package/lib/index.d.ts +0 -1
  24. package/lib/index.d.ts.map +1 -1
  25. package/lib/index.js +0 -1
  26. package/lib/index.js.map +1 -1
  27. package/package.json +25 -5
  28. package/src/g-definitions.ts +10 -1
  29. package/src/helpers/adopusNamespaceExchange.test.ts +117 -0
  30. package/src/helpers/adopusNamespaceExchange.ts +215 -0
  31. package/src/helpers/generateSrvAuthBindings.ts +7 -2
  32. package/src/helpers/generateSrvAuthBindings.types.ts +1 -1
  33. package/src/helpers/getDefaultAppVersions.test.ts +48 -0
  34. package/src/helpers/getDefaultAppVersions.ts +42 -0
  35. package/src/helpers/initASMAAppVitals.ts +14 -8
  36. package/src/helpers/isUUID.ts +1 -1
  37. package/src/index.ts +0 -1
  38. package/lib/helpers/generateGenqlClient.d.ts +0 -21
  39. package/lib/helpers/generateGenqlClient.d.ts.map +0 -1
  40. package/lib/helpers/generateGenqlClient.js +0 -98
  41. package/lib/helpers/generateGenqlClient.js.map +0 -1
  42. package/src/helpers/generateGenqlClient.ts +0 -145
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kCAAkC,CAAA;AAChD,cAAc,oCAAoC,CAAA;AAClD,cAAc,mCAAmC,CAAA;AACjD,cAAc,mCAAmC,CAAA;AACjD,cAAc,wBAAwB,CAAA;AACtC,cAAc,uBAAuB,CAAA;AACrC,cAAc,yBAAyB,CAAA;AACvC,cAAc,qBAAqB,CAAA;AACnC,cAAc,mDAAmD,CAAA;AACjE,cAAc,iCAAiC,CAAA;AAE/C,cAAc,kCAAkC,CAAA;AAChD,cAAc,uBAAuB,CAAA;AACrC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,wCAAwC,CAAA;AACtD,cAAc,sCAAsC,CAAA;AACpD,cAAc,4CAA4C,CAAA;AAC1D,cAAc,2BAA2B,CAAA;AACzC,cAAc,2CAA2C,CAAA;AACzD,cAAc,6BAA6B,CAAA;AAC3C,cAAc,kCAAkC,CAAA;AAChD,cAAc,yCAAyC,CAAA;AACvD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,2BAA2B,CAAA;AACzC,cAAc,yBAAyB,CAAA;AACvC,cAAc,2CAA2C,CAAA;AACzD,cAAc,gCAAgC,CAAA;AAC9C,cAAc,iCAAiC,CAAA;AAC/C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,+BAA+B,CAAA;AAE7C,cAAc,qBAAqB,CAAA;AACnC,cAAc,kCAAkC,CAAA;AAEhD,cAAc,oBAAoB,CAAA;AAElC,cAAc,sBAAsB,CAAA;AAEpC,cAAc,uBAAuB,CAAA;AAErC,cAAc,oBAAoB,CAAA;AAElC,cAAc,8CAA8C,CAAA;AAC5D,cAAc,+CAA+C,CAAA;AAC7D,cAAc,mCAAmC,CAAA;AACjD,cAAc,qBAAqB,CAAA;AACnC,cAAc,uCAAuC,CAAA;AACrD,cAAc,gCAAgC,CAAA;AAC9C,cAAc,kCAAkC,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kCAAkC,CAAA;AAChD,cAAc,oCAAoC,CAAA;AAClD,cAAc,mCAAmC,CAAA;AACjD,cAAc,mCAAmC,CAAA;AACjD,cAAc,wBAAwB,CAAA;AACtC,cAAc,uBAAuB,CAAA;AACrC,cAAc,yBAAyB,CAAA;AACvC,cAAc,qBAAqB,CAAA;AACnC,cAAc,mDAAmD,CAAA;AACjE,cAAc,iCAAiC,CAAA;AAE/C,cAAc,kCAAkC,CAAA;AAChD,cAAc,uBAAuB,CAAA;AACrC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,wCAAwC,CAAA;AACtD,cAAc,sCAAsC,CAAA;AACpD,cAAc,4CAA4C,CAAA;AAC1D,cAAc,2BAA2B,CAAA;AACzC,cAAc,2CAA2C,CAAA;AACzD,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yCAAyC,CAAA;AACvD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,2BAA2B,CAAA;AACzC,cAAc,yBAAyB,CAAA;AACvC,cAAc,2CAA2C,CAAA;AACzD,cAAc,gCAAgC,CAAA;AAC9C,cAAc,iCAAiC,CAAA;AAC/C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,+BAA+B,CAAA;AAE7C,cAAc,qBAAqB,CAAA;AACnC,cAAc,kCAAkC,CAAA;AAEhD,cAAc,oBAAoB,CAAA;AAElC,cAAc,sBAAsB,CAAA;AAEpC,cAAc,uBAAuB,CAAA;AAErC,cAAc,oBAAoB,CAAA;AAElC,cAAc,8CAA8C,CAAA;AAC5D,cAAc,+CAA+C,CAAA;AAC7D,cAAc,mCAAmC,CAAA;AACjD,cAAc,qBAAqB,CAAA;AACnC,cAAc,uCAAuC,CAAA;AACrD,cAAc,gCAAgC,CAAA;AAC9C,cAAc,kCAAkC,CAAA"}
package/lib/index.js CHANGED
@@ -17,7 +17,6 @@ export * from './helpers/generateSrvAuthBindings.types.js';
17
17
  export * from './helpers/getSubdomain.js';
18
18
  export * from './helpers/checkForRegisteredSubdomains.js';
19
19
  export * from './helpers/clearCacheData.js';
20
- export * from './helpers/generateGenqlClient.js';
21
20
  export * from './helpers/generateEnvConfigsBindings.js';
22
21
  export * from './helpers/userTypingSignal.js';
23
22
  export * from './helpers/base64ToFile.js';
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kCAAkC,CAAA;AAChD,cAAc,oCAAoC,CAAA;AAClD,cAAc,mCAAmC,CAAA;AACjD,cAAc,mCAAmC,CAAA;AACjD,cAAc,wBAAwB,CAAA;AACtC,cAAc,uBAAuB,CAAA;AACrC,cAAc,yBAAyB,CAAA;AACvC,cAAc,qBAAqB,CAAA;AACnC,cAAc,mDAAmD,CAAA;AACjE,cAAc,iCAAiC,CAAA;AAE/C,cAAc,kCAAkC,CAAA;AAChD,cAAc,uBAAuB,CAAA;AACrC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,wCAAwC,CAAA;AACtD,cAAc,sCAAsC,CAAA;AACpD,cAAc,4CAA4C,CAAA;AAC1D,cAAc,2BAA2B,CAAA;AACzC,cAAc,2CAA2C,CAAA;AACzD,cAAc,6BAA6B,CAAA;AAC3C,cAAc,kCAAkC,CAAA;AAChD,cAAc,yCAAyC,CAAA;AACvD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,2BAA2B,CAAA;AACzC,cAAc,yBAAyB,CAAA;AACvC,cAAc,2CAA2C,CAAA;AACzD,cAAc,gCAAgC,CAAA;AAC9C,cAAc,iCAAiC,CAAA;AAC/C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,+BAA+B,CAAA;AAC7C,gDAAgD;AAChD,cAAc,qBAAqB,CAAA;AACnC,cAAc,kCAAkC,CAAA;AAEhD,cAAc,oBAAoB,CAAA;AAElC,cAAc,sBAAsB,CAAA;AAEpC,cAAc,uBAAuB,CAAA;AAErC,cAAc,oBAAoB,CAAA;AAElC,cAAc,8CAA8C,CAAA;AAC5D,cAAc,+CAA+C,CAAA;AAC7D,cAAc,mCAAmC,CAAA;AACjD,cAAc,qBAAqB,CAAA;AACnC,cAAc,uCAAuC,CAAA;AACrD,cAAc,gCAAgC,CAAA;AAC9C,cAAc,kCAAkC,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kCAAkC,CAAA;AAChD,cAAc,oCAAoC,CAAA;AAClD,cAAc,mCAAmC,CAAA;AACjD,cAAc,mCAAmC,CAAA;AACjD,cAAc,wBAAwB,CAAA;AACtC,cAAc,uBAAuB,CAAA;AACrC,cAAc,yBAAyB,CAAA;AACvC,cAAc,qBAAqB,CAAA;AACnC,cAAc,mDAAmD,CAAA;AACjE,cAAc,iCAAiC,CAAA;AAE/C,cAAc,kCAAkC,CAAA;AAChD,cAAc,uBAAuB,CAAA;AACrC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,wCAAwC,CAAA;AACtD,cAAc,sCAAsC,CAAA;AACpD,cAAc,4CAA4C,CAAA;AAC1D,cAAc,2BAA2B,CAAA;AACzC,cAAc,2CAA2C,CAAA;AACzD,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yCAAyC,CAAA;AACvD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,2BAA2B,CAAA;AACzC,cAAc,yBAAyB,CAAA;AACvC,cAAc,2CAA2C,CAAA;AACzD,cAAc,gCAAgC,CAAA;AAC9C,cAAc,iCAAiC,CAAA;AAC/C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,+BAA+B,CAAA;AAC7C,gDAAgD;AAChD,cAAc,qBAAqB,CAAA;AACnC,cAAc,kCAAkC,CAAA;AAEhD,cAAc,oBAAoB,CAAA;AAElC,cAAc,sBAAsB,CAAA;AAEpC,cAAc,uBAAuB,CAAA;AAErC,cAAc,oBAAoB,CAAA;AAElC,cAAc,8CAA8C,CAAA;AAC5D,cAAc,+CAA+C,CAAA;AAC7D,cAAc,mCAAmC,CAAA;AACjD,cAAc,qBAAqB,CAAA;AACnC,cAAc,uCAAuC,CAAA;AACrD,cAAc,gCAAgC,CAAA;AAC9C,cAAc,kCAAkC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "asma-core-helpers",
3
- "version": "0.7.2",
3
+ "version": "0.9.0",
4
4
  "description": "Core helper utilities for ASMA applications",
5
5
  "files": [
6
6
  "lib/**/*",
@@ -10,10 +10,15 @@
10
10
  "type": "module",
11
11
  "exports": {
12
12
  ".": "./lib/index.js",
13
- "./lib": "./lib/index.js"
13
+ "./lib": "./lib/index.js",
14
+ "./urql": {
15
+ "types": "./lib/helpers/adopusNamespaceExchange.d.ts",
16
+ "default": "./lib/helpers/adopusNamespaceExchange.js"
17
+ }
14
18
  },
15
19
  "scripts": {
16
- "build": "npx tsc && npx tsc -p ."
20
+ "build": "npx tsc && npx tsc -p .",
21
+ "test": "node --import tsx --test \"src/**/*.test.ts\""
17
22
  },
18
23
  "repository": {
19
24
  "type": "git",
@@ -22,16 +27,31 @@
22
27
  "keywords": [],
23
28
  "author": "",
24
29
  "license": "MIT",
30
+ "peerDependencies": {
31
+ "@urql/core": ">=4",
32
+ "wonka": ">=6"
33
+ },
34
+ "peerDependenciesMeta": {
35
+ "@urql/core": {
36
+ "optional": true
37
+ },
38
+ "wonka": {
39
+ "optional": true
40
+ }
41
+ },
25
42
  "devDependencies": {
26
- "@genql/runtime": "^2.10.0",
27
43
  "@openreplay/tracker": "^16.4.10",
44
+ "@urql/core": "^6.0.3",
28
45
  "asma-qiankun-react-loader": "^0.8.2",
29
46
  "asma-types": "^2.8.0",
30
47
  "history": "^5.3.0",
31
48
  "lefthook": "^2.1.1",
32
- "typescript": "^5.6.3"
49
+ "tsx": "^4.23.0",
50
+ "typescript": "^5.6.3",
51
+ "wonka": "^6.3.4"
33
52
  },
34
53
  "dependencies": {
54
+ "@0no-co/graphql.web": "^1.0.9",
35
55
  "asma-event-bus": "^0.7.27",
36
56
  "buffer": "^6.0.3",
37
57
  "idb-keyval": "^6.2.1",
@@ -7,7 +7,7 @@ import type {
7
7
  import type { IGlobalOpenReplay } from './helpers/openReplayObject.js'
8
8
  import { realWindow } from './helpers/getSubdomain.js'
9
9
  import type { ICheckSigninOptions, ICheckSigninTransformedOptions } from './helpers/generateSrvAuthBindings.types.js'
10
- import type { IBaseJwtClaims } from 'asma-types/lib'
10
+ import type { IBaseJwtClaims } from 'asma-types'
11
11
  import type { ActivityStatus } from './helpers/getActivityStatus.js'
12
12
  //import type { IGenerateSRVAuthBindings } from './helpers/generateSrvAuthBindings'
13
13
  //import type { IGenerateSRVAuthBindings } from './helpers/generateSrvAuthBindings'
@@ -115,6 +115,15 @@ declare global {
115
115
  setTheme: (theme: string) => void
116
116
  }
117
117
  rawWindow?: typeof window
118
+ /**
119
+ * Server-injected (first-hit) per-user platform payload — see asma-static-server ASMA-7544.
120
+ * Present when `ESM_PLATFORM_INJECTION` is on; the seed both the old (qiankun) and the new
121
+ * (ESM) architecture read their app versions/bases from.
122
+ */
123
+ __ASMA_PLATFORM__?: {
124
+ default_app_versions?: Record<string, string>
125
+ apps?: Record<string, { version: string; base: string }>
126
+ }
118
127
  __ENV?: Record<string, string>
119
128
  __ENV_MICRO: {
120
129
  [key: string]: Record<string, string>
@@ -0,0 +1,117 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import { parse, print } from '@0no-co/graphql.web'
5
+
6
+ import { adopusSlugFromJwt, denamespaceData, namespaceDocument } from './adopusNamespaceExchange.js'
7
+
8
+ const PREFIXED = new Set(['Actor', 'Actor_bool_exp', 'Int_comparison_exp', 'uniqueidentifier'])
9
+
10
+ const normalize = (sdl: string) => print(parse(sdl))
11
+
12
+ describe('namespaceDocument (transforms 1-3)', () => {
13
+ it('wraps the operation, prefixes variable definition types, leaves builtins alone', () => {
14
+ const doc = parse(`
15
+ query fetchActors($where: Actor_bool_exp, $limit: Int, $id: uniqueidentifier) {
16
+ Actor(where: $where, limit: $limit) { ActorNo Navn }
17
+ }
18
+ `)
19
+ const out = print(namespaceDocument(doc, 'avansas', PREFIXED))
20
+ assert.equal(
21
+ out,
22
+ normalize(`
23
+ query fetchActors($where: avansas_Actor_bool_exp, $limit: Int, $id: avansas_uniqueidentifier) {
24
+ avansas {
25
+ Actor(where: $where, limit: $limit) { ActorNo Navn }
26
+ }
27
+ }
28
+ `),
29
+ )
30
+ })
31
+
32
+ it('prefixes wrapped list/non-null variable types', () => {
33
+ const doc = parse(`query q($ids: [Int_comparison_exp!]!) { Actor { ActorNo } }`)
34
+ const out = print(namespaceDocument(doc, 'avansas', PREFIXED))
35
+ assert.match(out, /\$ids: \[avansas_Int_comparison_exp!\]!/)
36
+ })
37
+
38
+ it('prefixes inline and named fragment type conditions, at any depth', () => {
39
+ const doc = parse(`
40
+ query q {
41
+ Actor {
42
+ ... on Actor { ActorNo }
43
+ ActorActor_Overordnet { ... on Actor { Navn } }
44
+ ...actorFields
45
+ }
46
+ }
47
+ fragment actorFields on Actor { Navn }
48
+ `)
49
+ const out = print(namespaceDocument(doc, 'avansas', PREFIXED))
50
+ assert.equal((out.match(/on avansas_Actor/g) ?? []).length, 3)
51
+ assert.doesNotMatch(out, /on Actor\b/)
52
+ })
53
+
54
+ it('wraps mutations too', () => {
55
+ const doc = parse(`mutation m($w: Actor_bool_exp!) { update_Actor(where: $w) { affected_rows } }`)
56
+ const out = print(namespaceDocument(doc, 'genesisdev', PREFIXED))
57
+ assert.equal(
58
+ out,
59
+ normalize(`
60
+ mutation m($w: genesisdev_Actor_bool_exp!) {
61
+ genesisdev { update_Actor(where: $w) { affected_rows } }
62
+ }
63
+ `),
64
+ )
65
+ })
66
+ })
67
+
68
+ describe('denamespaceData (transforms 4-5)', () => {
69
+ it('unwraps the namespace and strips __typename prefixes deeply', () => {
70
+ const wire = {
71
+ avansas: {
72
+ Actor: [
73
+ {
74
+ __typename: 'avansas_Actor',
75
+ ActorNo: 1,
76
+ ActorActor_Overordnet: [{ __typename: 'avansas_Actor', ActorNo: 2 }],
77
+ },
78
+ ],
79
+ },
80
+ }
81
+ assert.deepEqual(denamespaceData(wire, 'avansas'), {
82
+ Actor: [
83
+ {
84
+ __typename: 'Actor',
85
+ ActorNo: 1,
86
+ ActorActor_Overordnet: [{ __typename: 'Actor', ActorNo: 2 }],
87
+ },
88
+ ],
89
+ })
90
+ })
91
+
92
+ it('leaves unprefixed __typename values and non-typename fields untouched', () => {
93
+ const wire = { avansas: { Actor: [{ __typename: 'Actor', Navn: 'avansas_not_a_typename' }] } }
94
+ assert.deepEqual(denamespaceData(wire, 'avansas'), {
95
+ Actor: [{ __typename: 'Actor', Navn: 'avansas_not_a_typename' }],
96
+ })
97
+ })
98
+
99
+ it('passes data through when the namespace key is absent', () => {
100
+ assert.deepEqual(denamespaceData({ Actor: [] }, 'avansas'), { Actor: [] })
101
+ })
102
+ })
103
+
104
+ describe('adopusSlugFromJwt', () => {
105
+ const jwtWith = (claims: object) =>
106
+ `x.${Buffer.from(JSON.stringify(claims)).toString('base64url')}.y`
107
+
108
+ it('reads and sanitizes the subdomain claim', () => {
109
+ assert.equal(adopusSlugFromJwt(jwtWith({ subdomain: 'Fretex-Dev' })), 'fretex_dev')
110
+ })
111
+
112
+ it('returns undefined for missing token, missing claim, or unsanitizable subdomain', () => {
113
+ assert.equal(adopusSlugFromJwt(undefined), undefined)
114
+ assert.equal(adopusSlugFromJwt(jwtWith({})), undefined)
115
+ assert.equal(adopusSlugFromJwt(jwtWith({ subdomain: '1-bad!' })), undefined)
116
+ })
117
+ })
@@ -0,0 +1,215 @@
1
+ /**
2
+ * adopusNamespaceExchange — urql exchange that maps unprefixed gql.tada operations onto the
3
+ * consolidated Adopus Hasura engine's per-tenant namespace, at the wire level only.
4
+ *
5
+ * Compile-time types stay the unprefixed avansas/AdOpusTest schema (DEC-004/DEC-011); this
6
+ * exchange rewrites the parsed document + JSON per request/response:
7
+ * 1. request: wrap each operation's selection set in the `<slug>` root field
8
+ * 2. request: prefix schema type names in variable definitions (`Actor_bool_exp` -> `<slug>_Actor_bool_exp`)
9
+ * 3. request: prefix fragment type conditions (inline + named)
10
+ * 4. response: unwrap `data[<slug>]` -> `data`
11
+ * 5. response: strip the `<slug>_` prefix from `__typename` values
12
+ *
13
+ * `prefixedTypes` is the exact set of names the engine prefixes for the tenant source —
14
+ * generated at codegen time by adopus-hsr-wrapper/scripts/unwrap_client_schema.mjs
15
+ * (adopus-prefixed-types.json) so the runtime never guesses which names carry the prefix.
16
+ *
17
+ * Install it only on the SRV_AO_WRAPPER client and only when the resolved endpoint is the
18
+ * consolidated engine (`/api/hsr-wrapper/`) — the per-tenant `srv_urls.ao_wrapper` flip then
19
+ * switches endpoint and namespace mode atomically.
20
+ *
21
+ * @see asma-modules/_docs/adopus-graphql/plans/2026-07-11-00-14-plan-adopus-hasura2-consolidation.md:184 — §2.4.3 (DEC-004/DEC-015)
22
+ */
23
+ import type { DocumentNode } from '@0no-co/graphql.web'
24
+ import { makeOperation, type Exchange, type Operation, type OperationResult } from '@urql/core'
25
+ import { map, pipe } from 'wonka'
26
+
27
+ // Minimal structural AST model for exactly the nodes the transforms touch — graphql.web's
28
+ // published types carry `kind: any`, which defeats discriminant narrowing; these mirror the
29
+ // spec shape and the public API still speaks graphql.web's DocumentNode.
30
+ interface NameNode {
31
+ kind: 'Name'
32
+ value: string
33
+ }
34
+ interface NamedTypeNode {
35
+ kind: 'NamedType'
36
+ name: NameNode
37
+ }
38
+ type AstTypeNode = NamedTypeNode | { kind: 'ListType' | 'NonNullType'; type: AstTypeNode }
39
+ interface AstFieldNode {
40
+ kind: 'Field'
41
+ name: NameNode
42
+ selectionSet?: AstSelectionSetNode
43
+ }
44
+ interface AstInlineFragmentNode {
45
+ kind: 'InlineFragment'
46
+ typeCondition?: NamedTypeNode
47
+ selectionSet: AstSelectionSetNode
48
+ }
49
+ type AstSelectionNode = AstFieldNode | AstInlineFragmentNode | { kind: 'FragmentSpread'; name: NameNode }
50
+ interface AstSelectionSetNode {
51
+ kind: 'SelectionSet'
52
+ selections: readonly AstSelectionNode[]
53
+ }
54
+ interface AstVariableDefinitionNode {
55
+ kind: 'VariableDefinition'
56
+ type: AstTypeNode
57
+ }
58
+ interface AstOperationDefinitionNode {
59
+ kind: 'OperationDefinition'
60
+ variableDefinitions?: readonly AstVariableDefinitionNode[]
61
+ selectionSet: AstSelectionSetNode
62
+ }
63
+ interface AstFragmentDefinitionNode {
64
+ kind: 'FragmentDefinition'
65
+ typeCondition: NamedTypeNode
66
+ selectionSet: AstSelectionSetNode
67
+ }
68
+ type AstDefinitionNode = AstOperationDefinitionNode | AstFragmentDefinitionNode | { kind: string }
69
+ interface AstDocumentNode {
70
+ kind: 'Document'
71
+ definitions: readonly AstDefinitionNode[]
72
+ }
73
+
74
+ const SLUG_PATTERN = /^[a-z][a-z0-9_]{1,30}$/
75
+
76
+ // not parseJwt.js — that module touches `window` at import time; this subpath stays environment-neutral
77
+ function decodeJwtClaims(jwt: string): { subdomain?: string } | undefined {
78
+ const payload = jwt.split('.')[1]
79
+ if (!payload) return undefined
80
+ try {
81
+ const base64 = payload.replace(/-/g, '+').replace(/_/g, '/')
82
+ return JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(base64), (c) => c.charCodeAt(0))))
83
+ } catch {
84
+ return undefined
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Derive the tenant namespace slug from the main ASMA JWT's `subdomain` claim (DEC-015),
90
+ * sanitized per plan §1.1 (lowercase, hyphens -> underscores). Returns undefined when the
91
+ * token is missing or the claim does not sanitize to a valid slug — callers skip the
92
+ * exchange in that case.
93
+ */
94
+ export function adopusSlugFromJwt(jwt: string | undefined): string | undefined {
95
+ if (!jwt) return undefined
96
+ const claims = decodeJwtClaims(jwt)
97
+ const slug = claims?.subdomain?.toLowerCase().replace(/-/g, '_')
98
+ if (!slug || !SLUG_PATTERN.test(slug)) {
99
+ console.error(`adopusSlugFromJwt: subdomain claim '${claims?.subdomain}' is not a valid namespace slug`)
100
+ return undefined
101
+ }
102
+ return slug
103
+ }
104
+
105
+ /** Transforms 1–3 on the parsed operation document. Pure; exported for tests. */
106
+ export function namespaceDocument(document: DocumentNode, slug: string, prefixed: ReadonlySet<string>): DocumentNode {
107
+ const doc = document as unknown as AstDocumentNode
108
+
109
+ const prefixName = (name: NameNode): NameNode =>
110
+ prefixed.has(name.value) ? { ...name, value: `${slug}_${name.value}` } : name
111
+
112
+ const prefixType = (type: AstTypeNode): AstTypeNode =>
113
+ type.kind === 'NamedType' ? { ...type, name: prefixName(type.name) } : { ...type, type: prefixType(type.type) }
114
+
115
+ const prefixFragmentConditions = (selectionSet: AstSelectionSetNode): AstSelectionSetNode => ({
116
+ ...selectionSet,
117
+ selections: selectionSet.selections.map((sel): AstSelectionNode => {
118
+ if (sel.kind === 'InlineFragment') {
119
+ return {
120
+ ...sel,
121
+ typeCondition: sel.typeCondition && { ...sel.typeCondition, name: prefixName(sel.typeCondition.name) },
122
+ selectionSet: prefixFragmentConditions(sel.selectionSet),
123
+ }
124
+ }
125
+ if (sel.kind === 'Field' && sel.selectionSet) {
126
+ return { ...sel, selectionSet: prefixFragmentConditions(sel.selectionSet) }
127
+ }
128
+ return sel
129
+ }),
130
+ })
131
+
132
+ const out: AstDocumentNode = {
133
+ ...doc,
134
+ definitions: doc.definitions.map((def): AstDefinitionNode => {
135
+ if (def.kind === 'OperationDefinition') {
136
+ const op = def as AstOperationDefinitionNode
137
+ const namespaceField: AstFieldNode = {
138
+ kind: 'Field',
139
+ name: { kind: 'Name', value: slug },
140
+ selectionSet: prefixFragmentConditions(op.selectionSet),
141
+ }
142
+ return {
143
+ ...op,
144
+ variableDefinitions: op.variableDefinitions?.map((v) => ({ ...v, type: prefixType(v.type) })),
145
+ selectionSet: { kind: 'SelectionSet', selections: [namespaceField] },
146
+ }
147
+ }
148
+ if (def.kind === 'FragmentDefinition') {
149
+ const frag = def as AstFragmentDefinitionNode
150
+ return {
151
+ ...frag,
152
+ typeCondition: { ...frag.typeCondition, name: prefixName(frag.typeCondition.name) },
153
+ selectionSet: prefixFragmentConditions(frag.selectionSet),
154
+ }
155
+ }
156
+ return def
157
+ }),
158
+ }
159
+ return out as unknown as DocumentNode
160
+ }
161
+
162
+ /** Transforms 4–5 on the response payload. Pure (returns new objects); exported for tests. */
163
+ export function denamespaceData<T>(data: T, slug: string): T {
164
+ const unwrapped =
165
+ data && typeof data === 'object' && !Array.isArray(data) && slug in (data as Record<string, unknown>)
166
+ ? (data as Record<string, unknown>)[slug]
167
+ : data
168
+ return stripTypenamePrefix(unwrapped, `${slug}_`) as T
169
+ }
170
+
171
+ function stripTypenamePrefix(value: unknown, prefix: string): unknown {
172
+ if (Array.isArray(value)) return value.map((v) => stripTypenamePrefix(v, prefix))
173
+ if (!value || typeof value !== 'object') return value
174
+ const out: Record<string, unknown> = {}
175
+ for (const [key, v] of Object.entries(value)) {
176
+ out[key] = key === '__typename' && typeof v === 'string' && v.startsWith(prefix) ? v.slice(prefix.length) : stripTypenamePrefix(v, prefix)
177
+ }
178
+ return out
179
+ }
180
+
181
+ /**
182
+ * @param slug tenant namespace — derive with `adopusSlugFromJwt(await getCachedJwt())`
183
+ * @param prefixedTypes contents of the codegen-emitted `adopus-prefixed-types.json`
184
+ */
185
+ export function adopusNamespaceExchange({
186
+ slug,
187
+ prefixedTypes,
188
+ }: {
189
+ slug: string
190
+ prefixedTypes: readonly string[]
191
+ }): Exchange {
192
+ const prefixed: ReadonlySet<string> = new Set(prefixedTypes)
193
+ // documents are module-level gql.tada constants — stable references, so WeakMap memoizes
194
+ // the rewrite once per document (NOT per operation.key, which varies with variables)
195
+ const rewritten = new WeakMap<DocumentNode, DocumentNode>()
196
+
197
+ const rewriteOperation = (op: Operation): Operation => {
198
+ if (op.kind === 'teardown') return op
199
+ let query = rewritten.get(op.query)
200
+ if (!query) {
201
+ query = namespaceDocument(op.query, slug, prefixed)
202
+ rewritten.set(op.query, query)
203
+ }
204
+ return makeOperation(op.kind, { ...op, query }, op.context)
205
+ }
206
+
207
+ const rewriteResult = (result: OperationResult): OperationResult =>
208
+ result.operation.kind === 'teardown' || result.data == null
209
+ ? result // ponytail: error paths keep the <slug> segment — cosmetic, fix if it ever confuses anyone
210
+ : { ...result, data: denamespaceData(result.data, slug) }
211
+
212
+ return ({ forward }) =>
213
+ (ops$) =>
214
+ pipe(forward(pipe(ops$, map(rewriteOperation))), map(rewriteResult))
215
+ }
@@ -1,6 +1,7 @@
1
1
  import { asmaOverridesEventBus, EventBus } from 'asma-event-bus'
2
2
  import { EnvConfigsFnInternal } from './generateEnvConfigsBindings.js'
3
3
  import { realWindow } from './getSubdomain.js'
4
+ import { getInjectedPlatform } from './getDefaultAppVersions.js'
4
5
  import { get as _ } from 'idb-keyval'
5
6
  import type { ICheckSigninOptions, ICheckSigninTransformedOptions } from './generateSrvAuthBindings.types.js'
6
7
  import { domain, type ActivityStatus, type IAuthBindings } from '../index.js'
@@ -22,8 +23,8 @@ function dispatchJwtChangedEvent(jwt?: ICheckSigninOptions<any>) {
22
23
  dispatchSrvAuthEvents('jwt_changed', jwt, false)
23
24
  }
24
25
  /**
25
- * @generic FeatureEnums - feature_names_enums from asma-genql-directory
26
- * @generic SrvUrlsEnums - srv_names_enums from asma-genql-directory
26
+ * @generic FeatureEnums - feature_names_enums from directory introspection
27
+ * @generic SrvUrlsEnums - srv_names_enums from directory introspection
27
28
  */
28
29
  //type EnvConfigsFn = () => { SRV_AUTH: string; DEVELOPMENT: boolean; ENVIRONMENT_TO_OPERATE: string }
29
30
 
@@ -664,6 +665,10 @@ export function generateSrvAuthBindings<FE extends string>(logout?: () => void)
664
665
 
665
666
  realWindow.__ASMA__SHELL__.auth_bindings = auth_bindings
666
667
 
668
+ // Seed the overrides bus from the server-injected first-hit versions so the map is available
669
+ // before any auth response arrives (later responses still dispatch updates). ASMA-7544.
670
+ dispatchCustomerUserRelatedAppVersions(getInjectedPlatform()?.default_app_versions)
671
+
667
672
  return auth_bindings
668
673
  }
669
674
  /**
@@ -1,4 +1,4 @@
1
- import type { ITherapistOrSuperUserJwtClaims } from 'asma-types/lib'
1
+ import type { ITherapistOrSuperUserJwtClaims } from 'asma-types'
2
2
  type IOpenreplay = {
3
3
  enable: boolean
4
4
  block: boolean
@@ -0,0 +1,48 @@
1
+ import assert from 'node:assert/strict'
2
+ import { afterEach, describe, it } from 'node:test'
3
+
4
+ import { getDefaultAppVersions, getInjectedPlatform } from './getDefaultAppVersions.ts'
5
+
6
+ const globalWithWindow = globalThis as { window?: unknown }
7
+
8
+ afterEach(() => {
9
+ delete globalWithWindow.window
10
+ })
11
+
12
+ describe('getInjectedPlatform', () => {
13
+ it('returns undefined when there is no window (node/SSR)', () => {
14
+ assert.equal(getInjectedPlatform(), undefined)
15
+ })
16
+
17
+ it('reads window.__ASMA_PLATFORM__', () => {
18
+ globalWithWindow.window = { __ASMA_PLATFORM__: { default_app_versions: { 'asma-app-shell': '0.75.5' } } }
19
+ assert.deepEqual(getInjectedPlatform()?.default_app_versions, { 'asma-app-shell': '0.75.5' })
20
+ })
21
+
22
+ it('prefers window.rawWindow when present (qiankun child app)', () => {
23
+ globalWithWindow.window = {
24
+ rawWindow: { __ASMA_PLATFORM__: { default_app_versions: { 'asma-app-calendar': '1.2.3' } } },
25
+ }
26
+ assert.deepEqual(getInjectedPlatform()?.default_app_versions, { 'asma-app-calendar': '1.2.3' })
27
+ })
28
+ })
29
+
30
+ describe('getDefaultAppVersions', () => {
31
+ it('prefers the injected map over the fallback (first-hit wins)', () => {
32
+ globalWithWindow.window = { __ASMA_PLATFORM__: { default_app_versions: { a: '1.0.0' } } }
33
+ assert.deepEqual(getDefaultAppVersions({ a: '0.0.1' }), { a: '1.0.0' })
34
+ })
35
+
36
+ it('falls back to the auth-response value when nothing is injected', () => {
37
+ assert.deepEqual(getDefaultAppVersions({ a: '0.0.1' }), { a: '0.0.1' })
38
+ })
39
+
40
+ it('falls back when the injected map is empty (treated as absent)', () => {
41
+ globalWithWindow.window = { __ASMA_PLATFORM__: { default_app_versions: {} } }
42
+ assert.deepEqual(getDefaultAppVersions({ a: '0.0.1' }), { a: '0.0.1' })
43
+ })
44
+
45
+ it('returns undefined when neither injected nor fallback is present', () => {
46
+ assert.equal(getDefaultAppVersions(), undefined)
47
+ })
48
+ })
@@ -0,0 +1,42 @@
1
+ /**
2
+ * First-hit platform accessor.
3
+ *
4
+ * The server injects `window.__ASMA_PLATFORM__` at first hit (asma-static-server, ASMA-7544).
5
+ * Both the old (qiankun) and the new (ESM) architecture read their per-user app versions through
6
+ * this one accessor: the injected value is preferred, the auth-response value is the fallback — so
7
+ * versions no longer depend on the `checkForRegisteredSubdomain`/signin round-trip timing. Absent
8
+ * injection (flag off / old HTML), the fallback IS today's behaviour.
9
+ *
10
+ * @see _docs/frontend/plans/2026-07-02-14-25-plan-asma-static-server-esm-first-hit-compatibility.md:? — REQ-008, TASK-014
11
+ */
12
+
13
+ export interface InjectedPlatformApp {
14
+ version: string
15
+ base: string
16
+ }
17
+
18
+ export interface InjectedPlatform {
19
+ default_app_versions?: Record<string, string>
20
+ apps?: Record<string, InjectedPlatformApp>
21
+ }
22
+
23
+ /** Read the server-injected platform payload (qiankun-aware via `rawWindow`); undefined outside a browser. */
24
+ export function getInjectedPlatform(): InjectedPlatform | undefined {
25
+ if (typeof window === 'undefined') {
26
+ return undefined
27
+ }
28
+ const realWindow = (window as Window).rawWindow || window
29
+ return realWindow.__ASMA_PLATFORM__
30
+ }
31
+
32
+ /**
33
+ * The per-user `service -> version` map, preferring the server-injected first-hit value and
34
+ * falling back to a provided auth-response value.
35
+ */
36
+ export function getDefaultAppVersions(fallback?: Record<string, string>): Record<string, string> | undefined {
37
+ const injected = getInjectedPlatform()?.default_app_versions
38
+ if (injected && Object.keys(injected).length > 0) {
39
+ return injected
40
+ }
41
+ return fallback
42
+ }
@@ -5,6 +5,7 @@ import {
5
5
  type IResWithSubdomainOnError,
6
6
  } from './checkForRegisteredSubdomains.js'
7
7
  import { clearCacheData } from './clearCacheData.js'
8
+ import { getDefaultAppVersions } from './getDefaultAppVersions.js'
8
9
  import { EnvConfigsFnInternal } from './generateEnvConfigsBindings.js'
9
10
  import { getCachedJwtInternal, isJwtValidInternal, registerCallbackOnSrvAuthEvents } from './generateSrvAuthBindings.js'
10
11
  import { isNotEmptyObjArr } from './IsNotEmpty.js'
@@ -93,16 +94,21 @@ export async function initASMAAppVitals({
93
94
 
94
95
  let registry_urls: (typeof _registry_envs)['local'] | undefined = undefined
95
96
 
96
- if (
97
- resRegisteredSubdomain &&
98
- 'props' in resRegisteredSubdomain &&
99
- isNotEmptyObjArr(resRegisteredSubdomain.props.default_app_versions)
100
- ) {
101
- const default_app_versions = resRegisteredSubdomain.props!.default_app_versions!
97
+ // Prefer the server-injected first-hit versions (window.__ASMA_PLATFORM__); fall back to the
98
+ // signin/subdomain response value — same source of truth, no dependency on its round-trip timing.
99
+ const propsDefaultAppVersions =
100
+ resRegisteredSubdomain && 'props' in resRegisteredSubdomain
101
+ ? resRegisteredSubdomain.props.default_app_versions
102
+ : undefined
102
103
 
103
- registry_urls = Object.keys(default_app_versions).reduce(
104
+ const default_app_versions = getDefaultAppVersions(propsDefaultAppVersions)
105
+
106
+ if (default_app_versions && isNotEmptyObjArr(default_app_versions)) {
107
+ const resolved_versions = default_app_versions
108
+
109
+ registry_urls = Object.keys(resolved_versions).reduce(
104
110
  (acc, key) => {
105
- const app_version = default_app_versions[key]
111
+ const app_version = resolved_versions[key]
106
112
 
107
113
  const base_url = EnvConfigsFnInternal().CDN_ASMA_BASE_URL || '/cdn'
108
114
 
@@ -1,4 +1,4 @@
1
- import type { IUUID } from 'asma-types/lib'
1
+ import type { IUUID } from 'asma-types'
2
2
 
3
3
  const STRICT_UUID_REGEX: RegExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
4
4
  const ASMA_UUID_REGEX: RegExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
package/src/index.ts CHANGED
@@ -18,7 +18,6 @@ export * from './helpers/generateSrvAuthBindings.types.js'
18
18
  export * from './helpers/getSubdomain.js'
19
19
  export * from './helpers/checkForRegisteredSubdomains.js'
20
20
  export * from './helpers/clearCacheData.js'
21
- export * from './helpers/generateGenqlClient.js'
22
21
  export * from './helpers/generateEnvConfigsBindings.js'
23
22
  export * from './helpers/userTypingSignal.js'
24
23
  export * from './helpers/base64ToFile.js'
@@ -1,21 +0,0 @@
1
- import type { ClientOptions } from '@genql/runtime';
2
- import { type IEnvironmentUrlsGenQLOnly } from './generateEnvConfigsBindings.js';
3
- export interface CliOptions extends Omit<ClientOptions, 'url' | 'signal'> {
4
- anonymous?: boolean;
5
- }
6
- export declare function generateGenqlClient<T extends ReturnType<typeof createClient>>({ setReqConfig, createClient, service, path, }: {
7
- setReqConfig?: () => Promise<{
8
- headers: Record<string, string>;
9
- }>;
10
- createClient: (options?: ClientOptions | undefined) => T;
11
- service: keyof IEnvironmentUrlsGenQLOnly;
12
- path?: string;
13
- }): {
14
- getGenqlClient: () => Promise<T>;
15
- resetGenqlClient: () => void;
16
- genqlClient: (options?: CliOptions & {
17
- abortController?: AbortController;
18
- }) => Promise<T>;
19
- genqlClientWs: () => Promise<T>;
20
- };
21
- //# sourceMappingURL=generateGenqlClient.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"generateGenqlClient.d.ts","sourceRoot":"","sources":["../../src/helpers/generateGenqlClient.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,EAEH,KAAK,yBAAyB,EAGjC,MAAM,iCAAiC,CAAA;AAIxC,MAAM,WAAW,UAAW,SAAQ,IAAI,CAAC,aAAa,EAAE,KAAK,GAAG,QAAQ,CAAC;IACrE,SAAS,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,UAAU,CAAC,OAAO,YAAY,CAAC,EAAE,EAC3E,YAAY,EACZ,YAAY,EACZ,OAAO,EAEP,IAAoB,GACvB,EAAE;IACC,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC,CAAA;IACjE,YAAY,EAAE,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,SAAS,KAAK,CAAC,CAAA;IAExD,OAAO,EAAE,MAAM,yBAAyB,CAAA;IACxC,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB;;;4BA+DuC,UAAU,GAAG;QAAE,eAAe,CAAC,EAAE,eAAe,CAAA;KAAE,KAAQ,OAAO,CAAC,CAAC,CAAC;;EAqD3G"}