@pikku/inspector 0.12.75 → 0.12.81

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 (219) hide show
  1. package/CHANGELOG.md +330 -0
  2. package/dist/add/add-analytics.d.ts +9 -0
  3. package/dist/add/add-analytics.js +178 -0
  4. package/dist/add/add-auth.js +0 -54
  5. package/dist/add/add-feature-flag.d.ts +2 -0
  6. package/dist/add/add-feature-flag.js +162 -0
  7. package/dist/add/add-wire-addon.js +1 -0
  8. package/dist/add/add-wire-remote-addon.js +1 -0
  9. package/dist/index.d.ts +3 -1
  10. package/dist/index.js +3 -1
  11. package/dist/inspector.js +4 -0
  12. package/dist/surface/collect-surface.d.ts +58 -0
  13. package/dist/surface/collect-surface.js +613 -0
  14. package/dist/types.d.ts +20 -11
  15. package/dist/utils/filter-inspector-state.js +14 -0
  16. package/dist/utils/post-process.js +15 -0
  17. package/dist/utils/read-module-specifiers.d.ts +8 -0
  18. package/dist/utils/read-module-specifiers.js +23 -0
  19. package/dist/utils/read-tsconfig-out-dir.d.ts +5 -0
  20. package/dist/utils/read-tsconfig-out-dir.js +12 -0
  21. package/dist/utils/resolve-deploy-target.d.ts +7 -0
  22. package/dist/utils/resolve-deploy-target.js +15 -7
  23. package/dist/utils/serialize-inspector-state.d.ts +5 -0
  24. package/dist/utils/serialize-inspector-state.js +8 -0
  25. package/dist/visit.js +4 -0
  26. package/package.json +16 -6
  27. package/run-tests.sh +21 -6
  28. package/src/add/add-addon-bans.ts +0 -85
  29. package/src/add/add-agent-scorer.test.ts +0 -197
  30. package/src/add/add-agent-scorer.ts +0 -83
  31. package/src/add/add-agent.test.ts +0 -87
  32. package/src/add/add-agent.ts +0 -638
  33. package/src/add/add-approval-description.ts +0 -76
  34. package/src/add/add-auth.test.ts +0 -791
  35. package/src/add/add-auth.ts +0 -424
  36. package/src/add/add-channel.ts +0 -778
  37. package/src/add/add-cli-renderers.test.ts +0 -74
  38. package/src/add/add-cli.ts +0 -1006
  39. package/src/add/add-credential.ts +0 -186
  40. package/src/add/add-feature.test.ts +0 -306
  41. package/src/add/add-feature.ts +0 -253
  42. package/src/add/add-file-extends-core-type.ts +0 -66
  43. package/src/add/add-file-with-config.ts +0 -45
  44. package/src/add/add-file-with-factory.test.ts +0 -134
  45. package/src/add/add-file-with-factory.ts +0 -288
  46. package/src/add/add-functions.test.ts +0 -743
  47. package/src/add/add-functions.ts +0 -1548
  48. package/src/add/add-gateway.ts +0 -143
  49. package/src/add/add-http-route-ref.test.ts +0 -149
  50. package/src/add/add-http-route.ts +0 -530
  51. package/src/add/add-http-routes.ts +0 -471
  52. package/src/add/add-keyed-wiring.ts +0 -180
  53. package/src/add/add-mcp-prompt.ts +0 -157
  54. package/src/add/add-mcp-resource.ts +0 -175
  55. package/src/add/add-middleware.test.ts +0 -196
  56. package/src/add/add-middleware.ts +0 -879
  57. package/src/add/add-permission-auth.test.ts +0 -59
  58. package/src/add/add-permission.ts +0 -311
  59. package/src/add/add-personas.test.ts +0 -406
  60. package/src/add/add-personas.ts +0 -287
  61. package/src/add/add-queue-worker.ts +0 -117
  62. package/src/add/add-rpc-invocations.test.ts +0 -287
  63. package/src/add/add-rpc-invocations.ts +0 -221
  64. package/src/add/add-scenario-step.test.ts +0 -509
  65. package/src/add/add-schedule.ts +0 -120
  66. package/src/add/add-scope.test.ts +0 -419
  67. package/src/add/add-scope.ts +0 -221
  68. package/src/add/add-secret.ts +0 -10
  69. package/src/add/add-subject-scenario-steps.test.ts +0 -161
  70. package/src/add/add-system-role.test.ts +0 -297
  71. package/src/add/add-system-role.ts +0 -162
  72. package/src/add/add-trigger.ts +0 -165
  73. package/src/add/add-variable.ts +0 -10
  74. package/src/add/add-wire-addon.test.ts +0 -185
  75. package/src/add/add-wire-addon.ts +0 -146
  76. package/src/add/add-wire-remote-addon.ts +0 -80
  77. package/src/add/add-workflow-edge-cases.test.ts +0 -201
  78. package/src/add/add-workflow-fanout.test.ts +0 -166
  79. package/src/add/add-workflow-graph-foreach.test.ts +0 -174
  80. package/src/add/add-workflow-graph-input.test.ts +0 -94
  81. package/src/add/add-workflow-graph-notes.test.ts +0 -74
  82. package/src/add/add-workflow-graph.ts +0 -644
  83. package/src/add/add-workflow-on-error.test.ts +0 -118
  84. package/src/add/add-workflow-return-switch.test.ts +0 -121
  85. package/src/add/add-workflow-silent-drops.test.ts +0 -261
  86. package/src/add/add-workflow-sleep-expression.test.ts +0 -96
  87. package/src/add/add-workflow.test.ts +0 -155
  88. package/src/add/add-workflow.ts +0 -482
  89. package/src/add/addon-bans.test.ts +0 -121
  90. package/src/add/addon-contracts.test.ts +0 -221
  91. package/src/add/allow-escape-hatches.test.ts +0 -144
  92. package/src/add/audit-meta.test.ts +0 -75
  93. package/src/add/classified-sink-check.test.ts +0 -347
  94. package/src/add/config-value-metadata.test.ts +0 -263
  95. package/src/add/dynamic-import-check.test.ts +0 -119
  96. package/src/add/expect-eventually-scenario-only.test.ts +0 -108
  97. package/src/add/function-body-span.test.ts +0 -96
  98. package/src/add/inline-wiring-function.test.ts +0 -104
  99. package/src/add/pii-check.test.ts +0 -258
  100. package/src/add/rpc-type-cast.test.ts +0 -123
  101. package/src/add/scenario-instrumentation.test.ts +0 -89
  102. package/src/add/scenario-instrumentation.ts +0 -42
  103. package/src/add/scenario-marker.test.ts +0 -89
  104. package/src/add/scenario-not-extractable.test.ts +0 -159
  105. package/src/add/scenario-services.test.ts +0 -103
  106. package/src/add/scenario-skip.test.ts +0 -83
  107. package/src/add/validate-workflow-graph-addons.test.ts +0 -102
  108. package/src/add/wire-name-literal.test.ts +0 -117
  109. package/src/error-codes-emitted.test.ts +0 -76
  110. package/src/error-codes.ts +0 -145
  111. package/src/index.ts +0 -66
  112. package/src/inspector.ts +0 -575
  113. package/src/schema-generator.ts +0 -1
  114. package/src/source-file-cache.ts +0 -73
  115. package/src/tsconfig-paths.test.ts +0 -90
  116. package/src/types-map.ts +0 -141
  117. package/src/types.ts +0 -775
  118. package/src/utils/annotate-http-route-auth.test.ts +0 -161
  119. package/src/utils/annotate-http-route-auth.ts +0 -53
  120. package/src/utils/check-classified-sinks.ts +0 -215
  121. package/src/utils/check-pii-output.ts +0 -120
  122. package/src/utils/compute-diagnostics.test.ts +0 -106
  123. package/src/utils/compute-required-schemas.ts +0 -48
  124. package/src/utils/contract-hashes.test.ts +0 -553
  125. package/src/utils/contract-hashes.ts +0 -387
  126. package/src/utils/custom-types-generator.test.ts +0 -124
  127. package/src/utils/custom-types-generator.ts +0 -150
  128. package/src/utils/detect-schema-vendor.test.ts +0 -123
  129. package/src/utils/detect-schema-vendor.ts +0 -120
  130. package/src/utils/does-type-extend-core-type.ts +0 -53
  131. package/src/utils/ensure-function-metadata.ts +0 -385
  132. package/src/utils/extract-function-name.ts +0 -612
  133. package/src/utils/extract-function-node.ts +0 -70
  134. package/src/utils/extract-node-value.test.ts +0 -117
  135. package/src/utils/extract-node-value.ts +0 -172
  136. package/src/utils/extract-secret-usage.test.ts +0 -81
  137. package/src/utils/extract-secret-usage.ts +0 -73
  138. package/src/utils/extract-services.ts +0 -70
  139. package/src/utils/extract-surface-usage.test.ts +0 -147
  140. package/src/utils/extract-surface-usage.ts +0 -121
  141. package/src/utils/filter-inspector-state.test.ts +0 -2232
  142. package/src/utils/filter-inspector-state.ts +0 -1298
  143. package/src/utils/filter-utils.test.ts +0 -843
  144. package/src/utils/filter-utils.ts +0 -161
  145. package/src/utils/find-root-dir.ts +0 -68
  146. package/src/utils/find-zod-transform.test.ts +0 -70
  147. package/src/utils/find-zod-transform.ts +0 -103
  148. package/src/utils/get-exported-variable-name.ts +0 -48
  149. package/src/utils/get-files-and-methods.ts +0 -157
  150. package/src/utils/get-property-value.test.ts +0 -141
  151. package/src/utils/get-property-value.ts +0 -331
  152. package/src/utils/hash.ts +0 -26
  153. package/src/utils/literal-properties.ts +0 -97
  154. package/src/utils/load-addon-functions-meta.test.ts +0 -157
  155. package/src/utils/load-addon-functions-meta.ts +0 -508
  156. package/src/utils/load-addon-scopes.test.ts +0 -138
  157. package/src/utils/middleware.test.ts +0 -204
  158. package/src/utils/middleware.ts +0 -303
  159. package/src/utils/nested-project-filter.test.ts +0 -53
  160. package/src/utils/nested-project-filter.ts +0 -28
  161. package/src/utils/permissions-shorthand.test.ts +0 -79
  162. package/src/utils/permissions.test.ts +0 -125
  163. package/src/utils/permissions.ts +0 -129
  164. package/src/utils/post-process.test.ts +0 -1094
  165. package/src/utils/post-process.ts +0 -1604
  166. package/src/utils/resolve-addon-package.ts +0 -106
  167. package/src/utils/resolve-core-type.test.ts +0 -142
  168. package/src/utils/resolve-core-type.ts +0 -35
  169. package/src/utils/resolve-deploy-target.test.ts +0 -135
  170. package/src/utils/resolve-deploy-target.ts +0 -65
  171. package/src/utils/resolve-function-meta.ts +0 -25
  172. package/src/utils/resolve-function-types.ts +0 -42
  173. package/src/utils/resolve-identifier.ts +0 -46
  174. package/src/utils/resolve-ref-contract.ts +0 -71
  175. package/src/utils/resolve-versions.test.ts +0 -499
  176. package/src/utils/resolve-versions.ts +0 -228
  177. package/src/utils/schema-cache.test.ts +0 -124
  178. package/src/utils/schema-generator.test.ts +0 -272
  179. package/src/utils/schema-generator.ts +0 -780
  180. package/src/utils/secret-alias-services.test.ts +0 -146
  181. package/src/utils/secret-alias-services.ts +0 -58
  182. package/src/utils/serialize-inspector-state.ts +0 -742
  183. package/src/utils/serialize-mcp-json.ts +0 -150
  184. package/src/utils/serialize-middleware-groups-meta.ts +0 -36
  185. package/src/utils/serialize-openapi-json.ts +0 -277
  186. package/src/utils/serialize-permissions-groups-meta.ts +0 -7
  187. package/src/utils/single-declaration.ts +0 -57
  188. package/src/utils/type-utils.ts +0 -145
  189. package/src/utils/validate-auth-sessionless.ts +0 -30
  190. package/src/utils/validate-exposed-functions-gated.test.ts +0 -230
  191. package/src/utils/validate-exposed-functions-gated.ts +0 -103
  192. package/src/utils/validate-tags-resolve-to-middleware.test.ts +0 -108
  193. package/src/utils/validate-tags-resolve-to-middleware.ts +0 -48
  194. package/src/utils/workflow/derive-workflow-plan.test.ts +0 -178
  195. package/src/utils/workflow/derive-workflow-plan.ts +0 -117
  196. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +0 -1465
  197. package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +0 -71
  198. package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +0 -99
  199. package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +0 -161
  200. package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +0 -121
  201. package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +0 -116
  202. package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +0 -145
  203. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +0 -71
  204. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +0 -2254
  205. package/src/utils/workflow/dsl/index.ts +0 -11
  206. package/src/utils/workflow/dsl/patterns.ts +0 -440
  207. package/src/utils/workflow/dsl/validation.ts +0 -203
  208. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +0 -35
  209. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +0 -543
  210. package/src/utils/workflow/graph/fanout-node-identity.test.ts +0 -105
  211. package/src/utils/workflow/graph/finalize-workflow-wires.ts +0 -310
  212. package/src/utils/workflow/graph/finalize-workflows.test.ts +0 -50
  213. package/src/utils/workflow/graph/finalize-workflows.ts +0 -130
  214. package/src/utils/workflow/graph/index.ts +0 -28
  215. package/src/utils/workflow/graph/serialize-workflow-graph.ts +0 -240
  216. package/src/utils/workflow/graph/workflow-graph.types.ts +0 -281
  217. package/src/visit.ts +0 -181
  218. package/tsconfig.json +0 -19
  219. package/tsconfig.tsbuildinfo +0 -1
@@ -1,419 +0,0 @@
1
- import { strict as assert } from 'assert'
2
- import { describe, test } from 'node:test'
3
- import { mkdtemp, rm, writeFile } from 'node:fs/promises'
4
- import { tmpdir } from 'node:os'
5
- import { join } from 'node:path'
6
- import { inspect } from '../inspector.js'
7
- import { ErrorCode } from '../error-codes.js'
8
- import type { InspectorLogger } from '../types.js'
9
-
10
- const makeLogger = (criticals: Array<{ code: ErrorCode; message: string }>) =>
11
- ({
12
- debug: () => {},
13
- info: () => {},
14
- warn: () => {},
15
- error: () => {},
16
- diagnostic: ({ code, message }) => {
17
- criticals.push({ code, message })
18
- },
19
- critical: (code: ErrorCode, message: string) => {
20
- criticals.push({ code, message })
21
- },
22
- hasCriticalErrors: () => criticals.length > 0,
23
- }) satisfies InspectorLogger
24
-
25
- const inspectSource = async (source: string) => {
26
- const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-scope-'))
27
- const file = join(rootDir, 'scopes.ts')
28
- await writeFile(file, source)
29
- const criticals: Array<{ code: ErrorCode; message: string }> = []
30
- try {
31
- const state = await inspect(makeLogger(criticals), [file], { rootDir })
32
- return { state, criticals, file }
33
- } finally {
34
- await rm(rootDir, { recursive: true, force: true })
35
- }
36
- }
37
-
38
- /** Inspects several files at once, keyed by basename. */
39
- const inspectSources = async (sources: Record<string, string>) => {
40
- const rootDir = await mkdtemp(join(tmpdir(), 'pikku-add-scope-multi-'))
41
- const files: Record<string, string> = {}
42
- for (const [name, source] of Object.entries(sources)) {
43
- files[name] = join(rootDir, name)
44
- await writeFile(files[name]!, source)
45
- }
46
- const criticals: Array<{ code: ErrorCode; message: string }> = []
47
- try {
48
- const state = await inspect(makeLogger(criticals), Object.values(files), {
49
- rootDir,
50
- })
51
- return { state, criticals, files }
52
- } finally {
53
- await rm(rootDir, { recursive: true, force: true })
54
- }
55
- }
56
-
57
- describe('addScope inspector', () => {
58
- test('extracts a flat scope', async () => {
59
- const { state, criticals } = await inspectSource(
60
- [
61
- "import { defineScope } from '@pikku/core/scope'",
62
- 'defineScope({',
63
- ' admin: {',
64
- " displayName: 'Administration',",
65
- " description: 'Administrative access',",
66
- ' },',
67
- '})',
68
- ].join('\n')
69
- )
70
-
71
- assert.equal(criticals.length, 0)
72
- assert.equal(state.scopes.definitions.length, 1)
73
- assert.equal(state.scopes.definitions[0]!.name, 'admin')
74
- assert.equal(state.scopes.definitions[0]!.displayName, 'Administration')
75
- assert.equal(
76
- state.scopes.definitions[0]!.description,
77
- 'Administrative access'
78
- )
79
- })
80
-
81
- test('extracts a displayName at every depth', async () => {
82
- const { state, criticals } = await inspectSource(
83
- [
84
- "import { defineScope } from '@pikku/core/scope'",
85
- 'defineScope({',
86
- ' admin: {',
87
- " displayName: 'Administration',",
88
- ' scopes: {',
89
- ' invoices: {',
90
- " displayName: 'Invoice Management',",
91
- ' scopes: {',
92
- " create: { displayName: 'Create Invoices' },",
93
- ' },',
94
- ' },',
95
- ' },',
96
- ' },',
97
- '})',
98
- ].join('\n')
99
- )
100
-
101
- assert.equal(criticals.length, 0)
102
- const admin = state.scopes.definitions[0]!
103
- assert.equal(admin.displayName, 'Administration')
104
- const invoices = admin.scopes!.invoices!
105
- assert.equal(invoices.displayName, 'Invoice Management')
106
- assert.equal(invoices.scopes!.create!.displayName, 'Create Invoices')
107
- })
108
-
109
- test('extracts a nested scope tree', async () => {
110
- const { state, criticals } = await inspectSource(
111
- [
112
- "import { defineScope } from '@pikku/core/scope'",
113
- 'defineScope({',
114
- ' admin: {',
115
- ' scopes: {',
116
- ' invoices: {',
117
- " description: 'Invoice management',",
118
- ' scopes: {',
119
- " create: { description: 'Create invoices' },",
120
- ' },',
121
- ' },',
122
- ' },',
123
- ' },',
124
- '})',
125
- ].join('\n')
126
- )
127
-
128
- assert.equal(criticals.length, 0)
129
- assert.deepEqual(state.scopes.definitions[0]!.scopes, {
130
- invoices: {
131
- description: 'Invoice management',
132
- scopes: {
133
- create: { description: 'Create invoices' },
134
- },
135
- },
136
- })
137
- })
138
-
139
- test('records the source file', async () => {
140
- const { state, file } = await inspectSource(
141
- [
142
- "import { defineScope } from '@pikku/core/scope'",
143
- 'defineScope({ admin: {} })',
144
- ].join('\n')
145
- )
146
-
147
- assert.equal(state.scopes.definitions[0]!.sourceFile, file)
148
- assert.ok(state.scopes.files.has(file))
149
- })
150
-
151
- // The point of the keyed form: one call declares as many roots as you like,
152
- // and a root reads exactly like the nodes beneath it.
153
- test('extracts several roots from one declaration', async () => {
154
- const { state, criticals } = await inspectSource(
155
- [
156
- "import { defineScope } from '@pikku/core/scope'",
157
- 'defineScope({ admin: {}, billing: { scopes: { read: {} } } })',
158
- ].join('\n')
159
- )
160
-
161
- assert.equal(criticals.length, 0)
162
- assert.deepEqual(
163
- state.scopes.definitions.map((d) => d.name),
164
- ['admin', 'billing']
165
- )
166
- assert.deepEqual(state.scopes.definitions[1]!.scopes, { read: {} })
167
- })
168
-
169
- test('extracts several declarations', async () => {
170
- const { state } = await inspectSource(
171
- [
172
- "import { defineScope } from '@pikku/core/scope'",
173
- 'defineScope({ admin: {} })',
174
- 'defineScope({ billing: {} })',
175
- ].join('\n')
176
- )
177
-
178
- assert.deepEqual(
179
- state.scopes.definitions.map((d) => d.name),
180
- ['admin', 'billing']
181
- )
182
- })
183
-
184
- // The CLI generates a `defineScope` of its own — `user-admin.gen.ts` ships
185
- // the whole `admin` tree, and `@pikku/addon-console` spells it out again — so
186
- // a project that scaffolds user-admin declares scopes in more than one file
187
- // and every one of them has to survive.
188
- test('declarations in separate files all survive', async () => {
189
- const { state, criticals } = await inspectSources({
190
- 'user-admin.gen.ts': [
191
- "import { defineScope } from '@pikku/core/scope'",
192
- 'defineScope({ admin: {} })',
193
- ].join('\n'),
194
- 'scopes.ts': [
195
- "import { defineScope } from '@pikku/core/scope'",
196
- 'defineScope({ billing: {} })',
197
- ].join('\n'),
198
- })
199
-
200
- assert.deepEqual(
201
- criticals.filter((c) => /Only one defineScope/.test(c.message)),
202
- [],
203
- `expected no duplicate critical, got ${JSON.stringify(criticals)}`
204
- )
205
- assert.deepEqual(state.scopes.definitions.map((d) => d.name).sort(), [
206
- 'admin',
207
- 'billing',
208
- ])
209
- })
210
-
211
- // The CLI scaffolds a scope tree of its own — `user-admin.gen.ts` declares the
212
- // whole `admin` tree — so a generated declaration cannot be made to compete
213
- // with the app's own for the single slot.
214
- test('a generated declaration neither claims the slot nor conflicts', async () => {
215
- const { state, criticals } = await inspectSources({
216
- 'user-admin.gen.ts': [
217
- "import { defineScope } from '@pikku/core/scope'",
218
- 'defineScope({ admin: {} })',
219
- ].join('\n'),
220
- 'scopes.ts': [
221
- "import { defineScope } from '@pikku/core/scope'",
222
- 'defineScope({ billing: {} })',
223
- ].join('\n'),
224
- })
225
-
226
- assert.deepEqual(
227
- criticals.filter((c) => c.code === ErrorCode.DUPLICATE_SCOPE_DEFINITION),
228
- [],
229
- `expected no duplicate critical, got ${JSON.stringify(criticals)}`
230
- )
231
- assert.deepEqual(
232
- state.scopes.definitions.map((d) => d.name).sort(),
233
- ['admin', 'billing'],
234
- "both the generated tree and the app's own must survive"
235
- )
236
- })
237
-
238
- test('is critical when a root embeds the separator', async () => {
239
- const { criticals } = await inspectSource(
240
- [
241
- "import { defineScope } from '@pikku/core/scope'",
242
- "defineScope({ 'admin:users': {} })",
243
- ].join('\n')
244
- )
245
-
246
- assert.ok(
247
- criticals.some((c) => c.code === ErrorCode.INVALID_VALUE),
248
- `expected an INVALID_VALUE critical, got ${JSON.stringify(criticals)}`
249
- )
250
- })
251
-
252
- test('is critical when a root is the wildcard', async () => {
253
- const { criticals } = await inspectSource(
254
- [
255
- "import { defineScope } from '@pikku/core/scope'",
256
- "defineScope({ '*': {} })",
257
- ].join('\n')
258
- )
259
-
260
- assert.ok(
261
- criticals.some((c) => c.code === ErrorCode.INVALID_VALUE),
262
- `expected an INVALID_VALUE critical, got ${JSON.stringify(criticals)}`
263
- )
264
- })
265
-
266
- test('is critical when a root key is not a literal', async () => {
267
- const { criticals } = await inspectSource(
268
- [
269
- "import { defineScope } from '@pikku/core/scope'",
270
- 'const k = String(1)',
271
- 'defineScope({ [k]: {} } as any)',
272
- ].join('\n')
273
- )
274
-
275
- assert.ok(
276
- criticals.some((c) => c.code === ErrorCode.NON_LITERAL_WIRE_NAME),
277
- `expected a NON_LITERAL_WIRE_NAME critical, got ${JSON.stringify(criticals)}`
278
- )
279
- })
280
-
281
- test('is critical when a root is not an object literal', async () => {
282
- const { criticals } = await inspectSource(
283
- [
284
- "import { defineScope } from '@pikku/core/scope'",
285
- "defineScope({ admin: 'nope' } as any)",
286
- ].join('\n')
287
- )
288
-
289
- assert.ok(
290
- criticals.some((c) => c.code === ErrorCode.INVALID_VALUE),
291
- `expected an INVALID_VALUE critical, got ${JSON.stringify(criticals)}`
292
- )
293
- })
294
- })
295
-
296
- describe('validateScopeReferences', () => {
297
- const funcSource = (scopes: string, decls: string[] = []) =>
298
- [
299
- "import { defineScope } from '@pikku/core/scope'",
300
- "import { pikkuSessionlessFunc } from '@pikku/core'",
301
- ...decls,
302
- 'export const f = pikkuSessionlessFunc({',
303
- ` scopes: ${scopes},`,
304
- " func: async () => 'ok',",
305
- '})',
306
- ].join('\n')
307
-
308
- test('accepts a declared scope', async () => {
309
- const { criticals } = await inspectSource(
310
- funcSource("['admin:invoices:create']", [
311
- 'defineScope({',
312
- ' admin: {',
313
- ' scopes: { invoices: { scopes: { create: {} } } },',
314
- ' },',
315
- '})',
316
- ])
317
- )
318
-
319
- assert.deepEqual(criticals, [])
320
- })
321
-
322
- test('accepts an intermediate node of a declared tree', async () => {
323
- const { criticals } = await inspectSource(
324
- funcSource("['admin:invoices']", [
325
- 'defineScope({',
326
- ' admin: {',
327
- ' scopes: { invoices: { scopes: { create: {} } } },',
328
- ' },',
329
- '})',
330
- ])
331
- )
332
-
333
- assert.deepEqual(criticals, [])
334
- })
335
-
336
- test('rejects an undeclared scope', async () => {
337
- const { criticals } = await inspectSource(
338
- funcSource("['billing:read']", ['defineScope({ admin: {} })'])
339
- )
340
-
341
- assert.ok(
342
- criticals.some(
343
- (c) =>
344
- c.code === ErrorCode.INVALID_VALUE &&
345
- c.message.includes('billing:read')
346
- ),
347
- `expected an INVALID_VALUE critical for billing:read, got ${JSON.stringify(criticals)}`
348
- )
349
- })
350
-
351
- test('lists the available scopes when one is undeclared', async () => {
352
- const { criticals } = await inspectSource(
353
- funcSource("['nope']", ['defineScope({ admin: {} })'])
354
- )
355
-
356
- assert.ok(
357
- criticals.some((c) => c.message.includes('Available scopes: admin')),
358
- `expected the message to list available scopes, got ${JSON.stringify(criticals)}`
359
- )
360
- })
361
-
362
- test('rejects a typo in a nested scope', async () => {
363
- const { criticals } = await inspectSource(
364
- funcSource("['admin:invoice:create']", [
365
- 'defineScope({',
366
- ' admin: {',
367
- ' scopes: { invoices: { scopes: { create: {} } } },',
368
- ' },',
369
- '})',
370
- ])
371
- )
372
-
373
- assert.ok(
374
- criticals.some((c) => c.message.includes('admin:invoice:create')),
375
- `expected a critical for the typo, got ${JSON.stringify(criticals)}`
376
- )
377
- })
378
-
379
- test('accepts a wildcard requirement whose node is declared', async () => {
380
- const { criticals } = await inspectSource(
381
- funcSource("['admin:*']", ['defineScope({ admin: {} })'])
382
- )
383
-
384
- assert.deepEqual(criticals, [])
385
- })
386
-
387
- test('rejects a wildcard requirement whose node is undeclared', async () => {
388
- const { criticals } = await inspectSource(
389
- funcSource("['billing:*']", ['defineScope({ admin: {} })'])
390
- )
391
-
392
- assert.ok(
393
- criticals.some((c) => c.message.includes('billing:*')),
394
- `expected a critical for billing:*, got ${JSON.stringify(criticals)}`
395
- )
396
- })
397
-
398
- test('rejects a bare wildcard requirement', async () => {
399
- const { criticals } = await inspectSource(
400
- funcSource("['*']", ['defineScope({ admin: {} })'])
401
- )
402
-
403
- assert.ok(
404
- criticals.some((c) => c.message.includes('bare wildcard')),
405
- `expected a critical for the bare wildcard, got ${JSON.stringify(criticals)}`
406
- )
407
- })
408
-
409
- test('is silent for a function declaring no scopes', async () => {
410
- const { criticals } = await inspectSource(
411
- [
412
- "import { pikkuSessionlessFunc } from '@pikku/core'",
413
- "export const f = pikkuSessionlessFunc({ func: async () => 'ok' })",
414
- ].join('\n')
415
- )
416
-
417
- assert.deepEqual(criticals, [])
418
- })
419
- })
@@ -1,221 +0,0 @@
1
- import * as ts from 'typescript'
2
- import { getPropertyValue } from '../utils/get-property-value.js'
3
- import type { AddWiring, InspectorLogger } from '../types.js'
4
- import { ErrorCode } from '../error-codes.js'
5
- import type { ScopeNodeMeta } from '@pikku/core/scope'
6
-
7
- const SEPARATOR = ':'
8
- const WILDCARD = '*'
9
-
10
- /**
11
- * Validates a single scope segment, reporting rather than throwing so the
12
- * inspector can surface every problem in one pass.
13
- *
14
- * @returns true when the segment is usable.
15
- */
16
- const isValidSegment = (
17
- segment: string,
18
- scopeName: string,
19
- logger: InspectorLogger
20
- ): boolean => {
21
- if (segment.length === 0) {
22
- logger.critical(
23
- ErrorCode.INVALID_VALUE,
24
- `Scope '${scopeName}' contains an empty segment.`
25
- )
26
- return false
27
- }
28
- if (segment.includes(SEPARATOR)) {
29
- logger.critical(
30
- ErrorCode.INVALID_VALUE,
31
- `Scope segment '${segment}' in '${scopeName}' contains the '${SEPARATOR}' separator. ` +
32
- `Nest scopes with the 'scopes' property instead of embedding '${SEPARATOR}' in a name.`
33
- )
34
- return false
35
- }
36
- if (segment === WILDCARD) {
37
- logger.critical(
38
- ErrorCode.INVALID_VALUE,
39
- `Scope segment '${segment}' in '${scopeName}' is the wildcard. ` +
40
- `'${WILDCARD}' is reserved for granting a scope and its descendants, and cannot be declared.`
41
- )
42
- return false
43
- }
44
- return true
45
- }
46
-
47
- /**
48
- * Recursively extracts a `scopes: { ... }` object literal into nested metadata.
49
- */
50
- const extractScopeNodes = (
51
- obj: ts.ObjectLiteralExpression,
52
- scopeName: string,
53
- logger: InspectorLogger
54
- ): Record<string, ScopeNodeMeta> | undefined => {
55
- const nodes: Record<string, ScopeNodeMeta> = {}
56
-
57
- for (const prop of obj.properties) {
58
- if (!ts.isPropertyAssignment(prop)) {
59
- continue
60
- }
61
-
62
- let segment: string | undefined
63
- if (ts.isIdentifier(prop.name)) {
64
- segment = prop.name.text
65
- } else if (ts.isStringLiteral(prop.name)) {
66
- segment = prop.name.text
67
- }
68
-
69
- if (segment === undefined) {
70
- logger.critical(
71
- ErrorCode.NON_LITERAL_WIRE_NAME,
72
- `Scope '${scopeName}' has a nested scope whose key is not a literal.`
73
- )
74
- continue
75
- }
76
-
77
- if (!isValidSegment(segment, scopeName, logger)) {
78
- continue
79
- }
80
-
81
- if (!ts.isObjectLiteralExpression(prop.initializer)) {
82
- logger.critical(
83
- ErrorCode.INVALID_VALUE,
84
- `Nested scope '${segment}' in '${scopeName}' must be an object literal.`
85
- )
86
- continue
87
- }
88
-
89
- const node: ScopeNodeMeta = {}
90
- const displayName = getPropertyValue(prop.initializer, 'displayName') as
91
- string | null
92
- if (displayName) {
93
- node.displayName = displayName
94
- }
95
- const description = getPropertyValue(prop.initializer, 'description') as
96
- string | null
97
- if (description) {
98
- node.description = description
99
- }
100
-
101
- const nestedProp = prop.initializer.properties.find(
102
- (p) =>
103
- ts.isPropertyAssignment(p) &&
104
- ts.isIdentifier(p.name) &&
105
- p.name.text === 'scopes'
106
- )
107
- if (
108
- nestedProp &&
109
- ts.isPropertyAssignment(nestedProp) &&
110
- ts.isObjectLiteralExpression(nestedProp.initializer)
111
- ) {
112
- const nested = extractScopeNodes(
113
- nestedProp.initializer,
114
- scopeName,
115
- logger
116
- )
117
- if (nested) {
118
- node.scopes = nested
119
- }
120
- }
121
-
122
- nodes[segment] = node
123
- }
124
-
125
- return Object.keys(nodes).length > 0 ? nodes : undefined
126
- }
127
-
128
- /**
129
- * Unwraps `x as const` / `x as any` so a cast declaration is still extracted
130
- * rather than silently skipped.
131
- */
132
- const unwrapAs = (node: ts.Expression): ts.Expression =>
133
- ts.isAsExpression(node) || ts.isSatisfiesExpression(node)
134
- ? unwrapAs(node.expression)
135
- : node
136
-
137
- export const addScope: AddWiring = (logger, node, checker, state, _options) => {
138
- if (!ts.isCallExpression(node)) {
139
- return
140
- }
141
-
142
- const expression = node.expression
143
- if (!ts.isIdentifier(expression) || expression.text !== 'defineScope') {
144
- return
145
- }
146
-
147
- const firstArg = node.arguments[0]
148
- if (!firstArg) {
149
- return
150
- }
151
-
152
- const unwrapped = unwrapAs(firstArg)
153
- if (!ts.isObjectLiteralExpression(unwrapped)) {
154
- return
155
- }
156
-
157
- const sourceFile = node.getSourceFile().fileName
158
-
159
- // Roots are keyed exactly like the nodes beneath them, so each property of
160
- // the call's single argument is one tree.
161
- for (const prop of unwrapped.properties) {
162
- if (!ts.isPropertyAssignment(prop)) {
163
- continue
164
- }
165
-
166
- let name: string | undefined
167
- if (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) {
168
- name = prop.name.text
169
- }
170
-
171
- if (name === undefined) {
172
- logger.critical(
173
- ErrorCode.NON_LITERAL_WIRE_NAME,
174
- 'A scope is declared with a key that is not a literal.'
175
- )
176
- continue
177
- }
178
-
179
- if (!isValidSegment(name, name, logger)) {
180
- continue
181
- }
182
-
183
- if (!ts.isObjectLiteralExpression(prop.initializer)) {
184
- logger.critical(
185
- ErrorCode.INVALID_VALUE,
186
- `Scope '${name}' must be an object literal.`
187
- )
188
- continue
189
- }
190
-
191
- const root = prop.initializer
192
- const displayNameValue = getPropertyValue(root, 'displayName') as
193
- string | null
194
- const descriptionValue = getPropertyValue(root, 'description') as
195
- string | null
196
-
197
- let scopes: Record<string, ScopeNodeMeta> | undefined
198
- const scopesProp = root.properties.find(
199
- (p) =>
200
- ts.isPropertyAssignment(p) &&
201
- ts.isIdentifier(p.name) &&
202
- p.name.text === 'scopes'
203
- )
204
- if (
205
- scopesProp &&
206
- ts.isPropertyAssignment(scopesProp) &&
207
- ts.isObjectLiteralExpression(scopesProp.initializer)
208
- ) {
209
- scopes = extractScopeNodes(scopesProp.initializer, name, logger)
210
- }
211
-
212
- state.scopes.files.add(sourceFile)
213
- state.scopes.definitions.push({
214
- name,
215
- displayName: displayNameValue || undefined,
216
- description: descriptionValue || undefined,
217
- scopes,
218
- sourceFile,
219
- })
220
- }
221
- }
@@ -1,10 +0,0 @@
1
- import { createAddKeyedWiring } from './add-keyed-wiring.js'
2
-
3
- export const addSecret = createAddKeyedWiring({
4
- functionName: 'defineSecret',
5
- idField: 'secretId',
6
- label: 'Secret',
7
- schemaPrefix: 'SecretSchema',
8
- flagField: 'optional',
9
- getState: (state) => state.secrets,
10
- })