@pikku/core 0.12.4 → 0.12.5

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 (229) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/dist/dev/hot-reload.d.ts +10 -0
  3. package/dist/dev/hot-reload.js +158 -0
  4. package/dist/middleware-runner.d.ts +1 -0
  5. package/dist/middleware-runner.js +5 -0
  6. package/dist/permissions.d.ts +1 -0
  7. package/dist/permissions.js +5 -0
  8. package/dist/services/in-memory-workflow-service.js +7 -11
  9. package/dist/wirings/ai-agent/ai-agent-prepare.js +16 -1
  10. package/dist/wirings/channel/channel-middleware-runner.d.ts +1 -0
  11. package/dist/wirings/channel/channel-middleware-runner.js +5 -0
  12. package/dist/wirings/workflow/pikku-workflow-service.js +7 -8
  13. package/package.json +3 -2
  14. package/src/crypto-utils.test.ts +214 -0
  15. package/src/crypto-utils.ts +213 -0
  16. package/src/dev/hot-reload.test.ts +484 -0
  17. package/src/dev/hot-reload.ts +212 -0
  18. package/src/errors/error-handler.ts +62 -0
  19. package/src/errors/error.test.ts +195 -0
  20. package/src/errors/errors.ts +374 -0
  21. package/src/errors/index.ts +2 -0
  22. package/src/factory-functions.test.ts +109 -0
  23. package/src/function/function-runner.test.ts +536 -0
  24. package/src/function/function-runner.ts +365 -0
  25. package/src/function/functions.types.ts +304 -0
  26. package/src/function/index.ts +5 -0
  27. package/src/handle-error.test.ts +424 -0
  28. package/src/handle-error.ts +69 -0
  29. package/src/index.ts +118 -0
  30. package/src/internal.ts +6 -0
  31. package/src/middleware/auth-apikey.test.ts +363 -0
  32. package/src/middleware/auth-apikey.ts +47 -0
  33. package/src/middleware/auth-bearer.test.ts +450 -0
  34. package/src/middleware/auth-bearer.ts +72 -0
  35. package/src/middleware/auth-cookie.test.ts +528 -0
  36. package/src/middleware/auth-cookie.ts +80 -0
  37. package/src/middleware/cors.test.ts +424 -0
  38. package/src/middleware/cors.ts +104 -0
  39. package/src/middleware/index.ts +5 -0
  40. package/src/middleware/remote-auth.test.ts +488 -0
  41. package/src/middleware/remote-auth.ts +68 -0
  42. package/src/middleware/timeout.ts +15 -0
  43. package/src/middleware-runner.test.ts +418 -0
  44. package/src/middleware-runner.ts +240 -0
  45. package/src/permissions.test.ts +434 -0
  46. package/src/permissions.ts +327 -0
  47. package/src/pikku-request.ts +23 -0
  48. package/src/pikku-response.ts +5 -0
  49. package/src/pikku-state.test.ts +224 -0
  50. package/src/pikku-state.ts +216 -0
  51. package/src/run-tests-script.test.ts +49 -0
  52. package/src/schema.test.ts +249 -0
  53. package/src/schema.ts +151 -0
  54. package/src/services/ai-agent-runner-service.ts +41 -0
  55. package/src/services/ai-run-state-service.ts +20 -0
  56. package/src/services/ai-storage-service.ts +39 -0
  57. package/src/services/content-service.ts +76 -0
  58. package/src/services/deployment-service.ts +22 -0
  59. package/src/services/gateway-service.ts +20 -0
  60. package/src/services/gopass-secrets.ts +98 -0
  61. package/src/services/in-memory-ai-run-state-service.ts +73 -0
  62. package/src/services/in-memory-trigger-service.ts +64 -0
  63. package/src/services/in-memory-workflow-service.test.ts +351 -0
  64. package/src/services/in-memory-workflow-service.ts +502 -0
  65. package/src/services/index.ts +56 -0
  66. package/src/services/jwt-service.ts +30 -0
  67. package/src/services/local-content.ts +120 -0
  68. package/src/services/local-gateway-service.ts +62 -0
  69. package/src/services/local-secrets.test.ts +80 -0
  70. package/src/services/local-secrets.ts +94 -0
  71. package/src/services/local-variables.test.ts +61 -0
  72. package/src/services/local-variables.ts +39 -0
  73. package/src/services/logger-console.test.ts +118 -0
  74. package/src/services/logger-console.ts +98 -0
  75. package/src/services/logger.ts +57 -0
  76. package/src/services/scheduler-service.ts +86 -0
  77. package/src/services/schema-service.ts +33 -0
  78. package/src/services/scoped-secret-service.test.ts +74 -0
  79. package/src/services/scoped-secret-service.ts +41 -0
  80. package/src/services/secret-service.ts +38 -0
  81. package/src/services/trigger-service.ts +17 -0
  82. package/src/services/typed-secret-service.test.ts +93 -0
  83. package/src/services/typed-secret-service.ts +70 -0
  84. package/src/services/typed-variables-service.test.ts +73 -0
  85. package/src/services/typed-variables-service.ts +81 -0
  86. package/src/services/user-session-service.test.ts +113 -0
  87. package/src/services/user-session-service.ts +86 -0
  88. package/src/services/variables-service.ts +11 -0
  89. package/src/services/workflow-service.ts +95 -0
  90. package/src/testing/index.ts +2 -0
  91. package/src/testing/service-tests.ts +873 -0
  92. package/src/time-utils.test.ts +56 -0
  93. package/src/time-utils.ts +107 -0
  94. package/src/types/core.types.ts +478 -0
  95. package/src/types/state.types.ts +188 -0
  96. package/src/utils/hash.test.ts +68 -0
  97. package/src/utils/hash.ts +26 -0
  98. package/src/utils.test.ts +350 -0
  99. package/src/utils.ts +129 -0
  100. package/src/version.test.ts +80 -0
  101. package/src/version.ts +25 -0
  102. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
  103. package/src/wirings/ai-agent/ai-agent-helpers.test.ts +152 -0
  104. package/src/wirings/ai-agent/ai-agent-helpers.ts +76 -0
  105. package/src/wirings/ai-agent/ai-agent-memory.ts +310 -0
  106. package/src/wirings/ai-agent/ai-agent-model-config.test.ts +115 -0
  107. package/src/wirings/ai-agent/ai-agent-model-config.ts +43 -0
  108. package/src/wirings/ai-agent/ai-agent-prepare.ts +659 -0
  109. package/src/wirings/ai-agent/ai-agent-registry.test.ts +37 -0
  110. package/src/wirings/ai-agent/ai-agent-registry.ts +82 -0
  111. package/src/wirings/ai-agent/ai-agent-runner.test.ts +319 -0
  112. package/src/wirings/ai-agent/ai-agent-runner.ts +773 -0
  113. package/src/wirings/ai-agent/ai-agent-stream.test.ts +859 -0
  114. package/src/wirings/ai-agent/ai-agent-stream.ts +1153 -0
  115. package/src/wirings/ai-agent/ai-agent.types.ts +382 -0
  116. package/src/wirings/ai-agent/index.ts +37 -0
  117. package/src/wirings/channel/channel-common.ts +90 -0
  118. package/src/wirings/channel/channel-handler.ts +250 -0
  119. package/src/wirings/channel/channel-middleware-runner.ts +126 -0
  120. package/src/wirings/channel/channel-runner.ts +166 -0
  121. package/src/wirings/channel/channel-store.ts +29 -0
  122. package/src/wirings/channel/channel.types.ts +172 -0
  123. package/src/wirings/channel/define-channel-routes.ts +25 -0
  124. package/src/wirings/channel/eventhub-service.ts +34 -0
  125. package/src/wirings/channel/eventhub-store.ts +13 -0
  126. package/src/wirings/channel/index.ts +24 -0
  127. package/src/wirings/channel/local/index.ts +3 -0
  128. package/src/wirings/channel/local/local-channel-handler.ts +81 -0
  129. package/src/wirings/channel/local/local-channel-runner.test.ts +215 -0
  130. package/src/wirings/channel/local/local-channel-runner.ts +210 -0
  131. package/src/wirings/channel/local/local-eventhub-service.test.ts +95 -0
  132. package/src/wirings/channel/local/local-eventhub-service.ts +101 -0
  133. package/src/wirings/channel/log-channels.ts +20 -0
  134. package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +54 -0
  135. package/src/wirings/channel/pikku-abstract-channel-handler.ts +45 -0
  136. package/src/wirings/channel/serverless/index.ts +5 -0
  137. package/src/wirings/channel/serverless/serverless-channel-runner.ts +289 -0
  138. package/src/wirings/cli/channel/cli-channel-runner.ts +136 -0
  139. package/src/wirings/cli/channel/index.ts +1 -0
  140. package/src/wirings/cli/cli-runner.test.ts +403 -0
  141. package/src/wirings/cli/cli-runner.ts +529 -0
  142. package/src/wirings/cli/cli.types.ts +358 -0
  143. package/src/wirings/cli/command-parser.test.ts +445 -0
  144. package/src/wirings/cli/command-parser.ts +504 -0
  145. package/src/wirings/cli/define-cli-commands.ts +24 -0
  146. package/src/wirings/cli/index.ts +19 -0
  147. package/src/wirings/gateway/gateway-runner.test.ts +474 -0
  148. package/src/wirings/gateway/gateway-runner.ts +411 -0
  149. package/src/wirings/gateway/gateway.types.ts +149 -0
  150. package/src/wirings/gateway/index.ts +13 -0
  151. package/src/wirings/http/http-routes.test.ts +322 -0
  152. package/src/wirings/http/http-routes.ts +197 -0
  153. package/src/wirings/http/http-runner.test.ts +144 -0
  154. package/src/wirings/http/http-runner.ts +588 -0
  155. package/src/wirings/http/http.types.ts +369 -0
  156. package/src/wirings/http/index.ts +28 -0
  157. package/src/wirings/http/log-http-routes.ts +22 -0
  158. package/src/wirings/http/pikku-fetch-http-request.test.ts +237 -0
  159. package/src/wirings/http/pikku-fetch-http-request.ts +170 -0
  160. package/src/wirings/http/pikku-fetch-http-response.test.ts +82 -0
  161. package/src/wirings/http/pikku-fetch-http-response.ts +147 -0
  162. package/src/wirings/http/routers/http-router.ts +13 -0
  163. package/src/wirings/http/routers/path-to-regex.test.ts +319 -0
  164. package/src/wirings/http/routers/path-to-regex.ts +126 -0
  165. package/src/wirings/http/web-request.test.ts +236 -0
  166. package/src/wirings/http/web-request.ts +104 -0
  167. package/src/wirings/mcp/index.ts +27 -0
  168. package/src/wirings/mcp/mcp-endpoint-registry.test.ts +389 -0
  169. package/src/wirings/mcp/mcp-endpoint-registry.ts +159 -0
  170. package/src/wirings/mcp/mcp-runner.ts +323 -0
  171. package/src/wirings/mcp/mcp.types.ts +216 -0
  172. package/src/wirings/node/index.ts +2 -0
  173. package/src/wirings/node/node.types.ts +23 -0
  174. package/src/wirings/oauth2/index.ts +3 -0
  175. package/src/wirings/oauth2/oauth2-client.test.ts +929 -0
  176. package/src/wirings/oauth2/oauth2-client.ts +335 -0
  177. package/src/wirings/oauth2/oauth2.types.ts +69 -0
  178. package/src/wirings/oauth2/wire-oauth2-credential.ts +23 -0
  179. package/src/wirings/queue/index.ts +31 -0
  180. package/src/wirings/queue/queue-runner.test.ts +710 -0
  181. package/src/wirings/queue/queue-runner.ts +192 -0
  182. package/src/wirings/queue/queue.types.ts +189 -0
  183. package/src/wirings/queue/register-queue-helper.ts +60 -0
  184. package/src/wirings/queue/validate-worker-config.test.ts +108 -0
  185. package/src/wirings/queue/validate-worker-config.ts +116 -0
  186. package/src/wirings/rpc/index.ts +4 -0
  187. package/src/wirings/rpc/rpc-runner.ts +460 -0
  188. package/src/wirings/rpc/rpc-types.ts +56 -0
  189. package/src/wirings/rpc/wire-addon.ts +21 -0
  190. package/src/wirings/scheduler/index.ts +11 -0
  191. package/src/wirings/scheduler/log-schedulers.ts +20 -0
  192. package/src/wirings/scheduler/scheduler-runner.test.ts +660 -0
  193. package/src/wirings/scheduler/scheduler-runner.ts +133 -0
  194. package/src/wirings/scheduler/scheduler.types.ts +53 -0
  195. package/src/wirings/secret/index.ts +9 -0
  196. package/src/wirings/secret/secret.types.ts +32 -0
  197. package/src/wirings/secret/validate-secret-definitions.test.ts +140 -0
  198. package/src/wirings/secret/validate-secret-definitions.ts +82 -0
  199. package/src/wirings/trigger/index.ts +10 -0
  200. package/src/wirings/trigger/pikku-trigger-service.ts +112 -0
  201. package/src/wirings/trigger/trigger-runner.test.ts +79 -0
  202. package/src/wirings/trigger/trigger-runner.ts +135 -0
  203. package/src/wirings/trigger/trigger.types.ts +178 -0
  204. package/src/wirings/variable/index.ts +8 -0
  205. package/src/wirings/variable/validate-variable-definitions.test.ts +91 -0
  206. package/src/wirings/variable/validate-variable-definitions.ts +69 -0
  207. package/src/wirings/variable/variable.types.ts +22 -0
  208. package/src/wirings/workflow/dsl/index.ts +31 -0
  209. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +320 -0
  210. package/src/wirings/workflow/dsl/workflow-runner.ts +28 -0
  211. package/src/wirings/workflow/graph/graph-node.ts +222 -0
  212. package/src/wirings/workflow/graph/graph-runner.test.ts +487 -0
  213. package/src/wirings/workflow/graph/graph-runner.ts +816 -0
  214. package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
  215. package/src/wirings/workflow/graph/graph-validation.ts +237 -0
  216. package/src/wirings/workflow/graph/index.ts +19 -0
  217. package/src/wirings/workflow/graph/template.test.ts +49 -0
  218. package/src/wirings/workflow/graph/template.ts +42 -0
  219. package/src/wirings/workflow/graph/wire-workflow-graph.ts +29 -0
  220. package/src/wirings/workflow/graph/workflow-graph.types.ts +104 -0
  221. package/src/wirings/workflow/index.ts +82 -0
  222. package/src/wirings/workflow/pikku-workflow-service.test.ts +200 -0
  223. package/src/wirings/workflow/pikku-workflow-service.ts +1179 -0
  224. package/src/wirings/workflow/workflow-helpers.test.ts +129 -0
  225. package/src/wirings/workflow/workflow-helpers.ts +79 -0
  226. package/src/wirings/workflow/workflow.types.ts +274 -0
  227. package/tsconfig.json +14 -0
  228. package/tsconfig.tsbuildinfo +1 -0
  229. package/lcov.info +0 -18891
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Interface for a schema service that handles schema compilation, validation, and management.
3
+ */
4
+ export interface SchemaService {
5
+ /**
6
+ * Compiles a schema with the provided name and value, making it available for use.
7
+ * @param {string} name - The unique name for the schema.
8
+ * @param {any} value - The schema definition or configuration to be compiled.
9
+ * @returns {Promise<void> | void} A promise if asynchronous, or void if synchronous.
10
+ */
11
+ compileSchema: (name: string, value: any) => Promise<void> | void
12
+
13
+ /**
14
+ * Validates data against a specified schema.
15
+ * @param {string} schema - The name of the schema to validate against.
16
+ * @param {any} data - The data to validate against the schema.
17
+ * @returns {Promise<void> | void} A promise if asynchronous, or void if synchronous.
18
+ */
19
+ validateSchema: (schema: string, data: any) => Promise<void> | void
20
+
21
+ /**
22
+ * Retrieves a set of all registered schema names.
23
+ * @returns {Set<string>} A set containing the names of all available schemas.
24
+ */
25
+ getSchemaNames: () => Set<string>
26
+
27
+ /**
28
+ * Retrieves the property keys from a compiled JSON schema.
29
+ * @param {string} schemaName - The name of the schema.
30
+ * @returns {string[]} An array of property keys, or empty array if schema not found.
31
+ */
32
+ getSchemaKeys: (schemaName: string) => string[]
33
+ }
@@ -0,0 +1,74 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import { ScopedSecretService } from './scoped-secret-service.js'
4
+
5
+ const createMockSecrets = () => ({
6
+ getSecret: async (key: string) => `value-of-${key}`,
7
+ getSecretJSON: async (key: string) => ({ key }),
8
+ hasSecret: async () => true,
9
+ setSecretJSON: async () => {},
10
+ deleteSecret: async () => {},
11
+ })
12
+
13
+ describe('ScopedSecretService', () => {
14
+ test('should allow access to allowed keys', async () => {
15
+ const mock = createMockSecrets()
16
+ const scoped = new ScopedSecretService(mock, new Set(['KEY1', 'KEY2']))
17
+ const result = await scoped.getSecret('KEY1')
18
+ assert.strictEqual(result, 'value-of-KEY1')
19
+ })
20
+
21
+ test('should deny access to non-allowed keys', async () => {
22
+ const mock = createMockSecrets()
23
+ const scoped = new ScopedSecretService(mock, new Set(['KEY1']))
24
+ await assert.rejects(() => scoped.getSecret('KEY2'), {
25
+ message: 'Access denied to secret key: KEY2',
26
+ })
27
+ })
28
+
29
+ test('should allow getSecretJSON for allowed keys', async () => {
30
+ const mock = createMockSecrets()
31
+ const scoped = new ScopedSecretService(mock, new Set(['KEY1']))
32
+ const result = await scoped.getSecretJSON('KEY1')
33
+ assert.deepStrictEqual(result, { key: 'KEY1' })
34
+ })
35
+
36
+ test('should deny getSecretJSON for non-allowed keys', async () => {
37
+ const mock = createMockSecrets()
38
+ const scoped = new ScopedSecretService(mock, new Set(['KEY1']))
39
+ await assert.rejects(() => scoped.getSecretJSON('FORBIDDEN'), {
40
+ message: 'Access denied to secret key: FORBIDDEN',
41
+ })
42
+ })
43
+
44
+ test('should allow hasSecret for allowed keys', async () => {
45
+ const mock = createMockSecrets()
46
+ const scoped = new ScopedSecretService(mock, new Set(['KEY1']))
47
+ const result = await scoped.hasSecret('KEY1')
48
+ assert.strictEqual(result, true)
49
+ })
50
+
51
+ test('should deny hasSecret for non-allowed keys', async () => {
52
+ const mock = createMockSecrets()
53
+ const scoped = new ScopedSecretService(mock, new Set(['KEY1']))
54
+ await assert.rejects(() => scoped.hasSecret('KEY2'), {
55
+ message: 'Access denied to secret key: KEY2',
56
+ })
57
+ })
58
+
59
+ test('should always throw on setSecretJSON', async () => {
60
+ const mock = createMockSecrets()
61
+ const scoped = new ScopedSecretService(mock, new Set(['KEY1']))
62
+ await assert.rejects(() => scoped.setSecretJSON('KEY1', 'val'), {
63
+ message: 'setSecretJSON is not allowed in scoped secret service',
64
+ })
65
+ })
66
+
67
+ test('should always throw on deleteSecret', async () => {
68
+ const mock = createMockSecrets()
69
+ const scoped = new ScopedSecretService(mock, new Set(['KEY1']))
70
+ await assert.rejects(() => scoped.deleteSecret('KEY1'), {
71
+ message: 'deleteSecret is not allowed in scoped secret service',
72
+ })
73
+ })
74
+ })
@@ -0,0 +1,41 @@
1
+ import type { SecretService } from './secret-service.js'
2
+
3
+ /**
4
+ * A read-only wrapper around SecretService that restricts access
5
+ * to only a defined set of allowed secret keys.
6
+ */
7
+ export class ScopedSecretService implements SecretService {
8
+ constructor(
9
+ private secrets: SecretService,
10
+ private allowedKeys: Set<string>
11
+ ) {}
12
+
13
+ private assertAllowed(key: string): void {
14
+ if (!this.allowedKeys.has(key)) {
15
+ throw new Error(`Access denied to secret key: ${key}`)
16
+ }
17
+ }
18
+
19
+ async getSecret(key: string): Promise<string> {
20
+ this.assertAllowed(key)
21
+ return this.secrets.getSecret(key)
22
+ }
23
+
24
+ async getSecretJSON<T = {}>(key: string): Promise<T> {
25
+ this.assertAllowed(key)
26
+ return this.secrets.getSecretJSON<T>(key)
27
+ }
28
+
29
+ async hasSecret(key: string): Promise<boolean> {
30
+ this.assertAllowed(key)
31
+ return this.secrets.hasSecret(key)
32
+ }
33
+
34
+ async setSecretJSON(_key: string, _value: unknown): Promise<void> {
35
+ throw new Error('setSecretJSON is not allowed in scoped secret service')
36
+ }
37
+
38
+ async deleteSecret(_key: string): Promise<void> {
39
+ throw new Error('deleteSecret is not allowed in scoped secret service')
40
+ }
41
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Interface for retrieving and managing secrets.
3
+ */
4
+ export interface SecretService {
5
+ /**
6
+ * Retrieves a secret by key and parses it as JSON.
7
+ * @param key - The key of the secret to retrieve.
8
+ * @returns A promise that resolves to the parsed secret value.
9
+ * @throws If the secret is not found.
10
+ */
11
+ getSecretJSON<Return = {}>(key: string): Promise<Return>
12
+ /**
13
+ * Retrieves a secret by key as a string.
14
+ * @param key - The key of the secret to retrieve.
15
+ * @returns A promise that resolves to the secret value.
16
+ * @throws If the secret is not found.
17
+ */
18
+ getSecret(key: string): Promise<string>
19
+ /**
20
+ * Checks if a secret exists without throwing.
21
+ * @param key - The key of the secret to check.
22
+ * @returns A promise that resolves to true if the secret exists, false otherwise.
23
+ */
24
+ hasSecret(key: string): Promise<boolean>
25
+ /**
26
+ * Stores a JSON value as a secret.
27
+ * @param key - The key to store the secret under.
28
+ * @param value - The JSON value to store.
29
+ * @returns A promise that resolves when the secret is stored.
30
+ */
31
+ setSecretJSON(key: string, value: unknown): Promise<void>
32
+ /**
33
+ * Deletes a secret by key.
34
+ * @param key - The key of the secret to delete.
35
+ * @returns A promise that resolves when the secret is deleted.
36
+ */
37
+ deleteSecret(key: string): Promise<void>
38
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Abstract TriggerService interface.
3
+ *
4
+ * Implementations manage trigger subscriptions — listening to external events
5
+ * and dispatching to RPC targets or workflow starts.
6
+ */
7
+ export interface TriggerService {
8
+ /**
9
+ * Start all triggers
10
+ */
11
+ start(): Promise<void>
12
+
13
+ /**
14
+ * Stop all triggers
15
+ */
16
+ stop(): Promise<void>
17
+ }
@@ -0,0 +1,93 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import { TypedSecretService } from './typed-secret-service.js'
4
+
5
+ const createMockSecrets = (store: Map<string, string> = new Map()) => ({
6
+ getSecret: async (key: string) => {
7
+ const val = store.get(key)
8
+ if (!val) throw new Error(`Not found: ${key}`)
9
+ return val
10
+ },
11
+ getSecretJSON: async (key: string) => {
12
+ const val = store.get(key)
13
+ if (!val) throw new Error(`Not found: ${key}`)
14
+ return JSON.parse(val)
15
+ },
16
+ hasSecret: async (key: string) => store.has(key),
17
+ setSecretJSON: async (key: string, value: unknown) => {
18
+ store.set(key, JSON.stringify(value))
19
+ },
20
+ deleteSecret: async (key: string) => {
21
+ store.delete(key)
22
+ },
23
+ })
24
+
25
+ describe('TypedSecretService', () => {
26
+ test('should delegate getSecret to underlying service', async () => {
27
+ const store = new Map([['API_KEY', 'sk-123']])
28
+ const service = new TypedSecretService(createMockSecrets(store), {})
29
+ const result = await service.getSecret('API_KEY')
30
+ assert.strictEqual(result, 'sk-123')
31
+ })
32
+
33
+ test('should delegate getSecretJSON to underlying service', async () => {
34
+ const store = new Map([['CONFIG', '{"port":3000}']])
35
+ const service = new TypedSecretService(createMockSecrets(store), {})
36
+ const result = await service.getSecretJSON('CONFIG')
37
+ assert.deepStrictEqual(result, { port: 3000 })
38
+ })
39
+
40
+ test('should delegate hasSecret to underlying service', async () => {
41
+ const store = new Map([['EXISTS', 'val']])
42
+ const service = new TypedSecretService(createMockSecrets(store), {})
43
+ assert.strictEqual(await service.hasSecret('EXISTS'), true)
44
+ assert.strictEqual(await service.hasSecret('MISSING'), false)
45
+ })
46
+
47
+ test('should delegate setSecretJSON to underlying service', async () => {
48
+ const store = new Map<string, string>()
49
+ const service = new TypedSecretService(createMockSecrets(store), {})
50
+ await service.setSecretJSON('NEW_KEY', { data: 'val' })
51
+ assert.strictEqual(store.get('NEW_KEY'), '{"data":"val"}')
52
+ })
53
+
54
+ test('should delegate deleteSecret to underlying service', async () => {
55
+ const store = new Map([['KEY', 'val']])
56
+ const service = new TypedSecretService(createMockSecrets(store), {})
57
+ await service.deleteSecret('KEY')
58
+ assert.strictEqual(store.has('KEY'), false)
59
+ })
60
+
61
+ test('should get all status for credentials', async () => {
62
+ const store = new Map([['STRIPE_KEY', 'sk-123']])
63
+ const meta = {
64
+ STRIPE_KEY: { name: 'stripe', displayName: 'Stripe' },
65
+ GITHUB_TOKEN: {
66
+ name: 'github',
67
+ displayName: 'GitHub',
68
+ oauth2: { tokenSecretId: 'GH_TOKEN' },
69
+ },
70
+ }
71
+ const service = new TypedSecretService(createMockSecrets(store), meta)
72
+ const status = await service.getAllStatus()
73
+ assert.strictEqual(status.length, 2)
74
+ const stripeStatus = status.find((s) => s.secretId === 'STRIPE_KEY')!
75
+ assert.strictEqual(stripeStatus.isConfigured, true)
76
+ assert.strictEqual(stripeStatus.displayName, 'Stripe')
77
+ const ghStatus = status.find((s) => s.secretId === 'GITHUB_TOKEN')!
78
+ assert.strictEqual(ghStatus.isConfigured, false)
79
+ assert.deepStrictEqual(ghStatus.oauth2, { tokenSecretId: 'GH_TOKEN' })
80
+ })
81
+
82
+ test('should get missing credentials', async () => {
83
+ const store = new Map([['STRIPE_KEY', 'sk-123']])
84
+ const meta = {
85
+ STRIPE_KEY: { name: 'stripe', displayName: 'Stripe' },
86
+ GITHUB_TOKEN: { name: 'github', displayName: 'GitHub' },
87
+ }
88
+ const service = new TypedSecretService(createMockSecrets(store), meta)
89
+ const missing = await service.getMissing()
90
+ assert.strictEqual(missing.length, 1)
91
+ assert.strictEqual(missing[0].secretId, 'GITHUB_TOKEN')
92
+ })
93
+ })
@@ -0,0 +1,70 @@
1
+ import type { SecretService } from './secret-service.js'
2
+
3
+ export interface CredentialStatus {
4
+ secretId: string
5
+ name: string
6
+ displayName: string
7
+ isConfigured: boolean
8
+ oauth2?: { tokenSecretId: string }
9
+ }
10
+
11
+ export type CredentialMeta = {
12
+ name: string
13
+ displayName: string
14
+ oauth2?: { tokenSecretId: string }
15
+ }
16
+
17
+ export class TypedSecretService<TMap = Record<string, unknown>>
18
+ implements SecretService
19
+ {
20
+ constructor(
21
+ private secrets: SecretService,
22
+ private credentialsMeta: Record<string, CredentialMeta>
23
+ ) {}
24
+
25
+ async getSecretJSON<K extends keyof TMap & string>(key: K): Promise<TMap[K]>
26
+ async getSecretJSON<T = unknown>(key: string): Promise<T>
27
+ async getSecretJSON(key: string): Promise<unknown> {
28
+ return this.secrets.getSecretJSON(key)
29
+ }
30
+
31
+ async getSecret(key: string): Promise<string> {
32
+ return this.secrets.getSecret(key)
33
+ }
34
+
35
+ async hasSecret(key: string): Promise<boolean> {
36
+ return this.secrets.hasSecret(key)
37
+ }
38
+
39
+ async setSecretJSON<K extends string>(
40
+ key: K,
41
+ value: K extends keyof TMap ? TMap[K] : unknown
42
+ ): Promise<void> {
43
+ return this.secrets.setSecretJSON(key, value)
44
+ }
45
+
46
+ async deleteSecret(key: string): Promise<void> {
47
+ return this.secrets.deleteSecret(key)
48
+ }
49
+
50
+ async getAllStatus(): Promise<CredentialStatus[]> {
51
+ const results: CredentialStatus[] = []
52
+
53
+ for (const [secretId, meta] of Object.entries(this.credentialsMeta)) {
54
+ results.push({
55
+ secretId,
56
+ name: meta.name,
57
+ displayName: meta.displayName,
58
+ isConfigured: await this.secrets.hasSecret(secretId),
59
+ oauth2: meta.oauth2,
60
+ })
61
+ }
62
+
63
+ return results
64
+ }
65
+
66
+ async getMissing(): Promise<CredentialStatus[]> {
67
+ const all = await this.getAllStatus()
68
+ return all.filter((c) => !c.isConfigured)
69
+ }
70
+ }
@@ -0,0 +1,73 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import { TypedVariablesService } from './typed-variables-service.js'
4
+ import { LocalVariablesService } from './local-variables.js'
5
+
6
+ describe('TypedVariablesService', () => {
7
+ const createService = (vars: Record<string, string | undefined> = {}) => {
8
+ const underlying = new LocalVariablesService(vars)
9
+ const meta = {
10
+ DB_URL: { name: 'DB_URL', displayName: 'Database URL' },
11
+ API_KEY: { name: 'API_KEY', displayName: 'API Key' },
12
+ }
13
+ return new TypedVariablesService(underlying, meta)
14
+ }
15
+
16
+ test('should delegate get to underlying service', () => {
17
+ const service = createService({ DB_URL: 'postgres://...' })
18
+ assert.strictEqual(service.get('DB_URL'), 'postgres://...')
19
+ })
20
+
21
+ test('should delegate set to underlying service', () => {
22
+ const service = createService()
23
+ service.set('DB_URL', 'new-value')
24
+ assert.strictEqual(service.get('DB_URL'), 'new-value')
25
+ })
26
+
27
+ test('should delegate has to underlying service', () => {
28
+ const service = createService({ DB_URL: 'val' })
29
+ assert.strictEqual(service.has('DB_URL'), true)
30
+ assert.strictEqual(service.has('MISSING'), false)
31
+ })
32
+
33
+ test('should delegate delete to underlying service', () => {
34
+ const service = createService({ DB_URL: 'val' })
35
+ service.delete('DB_URL')
36
+ assert.strictEqual(service.has('DB_URL'), false)
37
+ })
38
+
39
+ test('should delegate getJSON to underlying service', () => {
40
+ const service = createService({ DATA: '{"key":"val"}' })
41
+ assert.deepStrictEqual(service.getJSON('DATA'), { key: 'val' })
42
+ })
43
+
44
+ test('should delegate setJSON to underlying service', () => {
45
+ const service = createService()
46
+ service.setJSON('DATA', { key: 'val' })
47
+ assert.strictEqual(service.get('DATA'), '{"key":"val"}')
48
+ })
49
+
50
+ test('should get all status for configured variables', async () => {
51
+ const service = createService({ DB_URL: 'postgres://...' })
52
+ const status = await service.getAllStatus()
53
+ assert.strictEqual(status.length, 2)
54
+ const dbStatus = status.find((s) => s.variableId === 'DB_URL')!
55
+ assert.strictEqual(dbStatus.isConfigured, true)
56
+ assert.strictEqual(dbStatus.displayName, 'Database URL')
57
+ const apiStatus = status.find((s) => s.variableId === 'API_KEY')!
58
+ assert.strictEqual(apiStatus.isConfigured, false)
59
+ })
60
+
61
+ test('should get missing variables', async () => {
62
+ const service = createService({ DB_URL: 'val' })
63
+ const missing = await service.getMissing()
64
+ assert.strictEqual(missing.length, 1)
65
+ assert.strictEqual(missing[0].variableId, 'API_KEY')
66
+ })
67
+
68
+ test('should return empty missing when all configured', async () => {
69
+ const service = createService({ DB_URL: 'val', API_KEY: 'key' })
70
+ const missing = await service.getMissing()
71
+ assert.strictEqual(missing.length, 0)
72
+ })
73
+ })
@@ -0,0 +1,81 @@
1
+ import type { VariablesService } from './variables-service.js'
2
+
3
+ export interface VariableStatus {
4
+ variableId: string
5
+ name: string
6
+ displayName: string
7
+ isConfigured: boolean
8
+ }
9
+
10
+ export type VariableMeta = {
11
+ name: string
12
+ displayName: string
13
+ }
14
+
15
+ export class TypedVariablesService<TMap = Record<string, unknown>>
16
+ implements VariablesService
17
+ {
18
+ constructor(
19
+ private variables: VariablesService,
20
+ private variablesMeta: Record<string, VariableMeta>
21
+ ) {}
22
+
23
+ get(
24
+ name: keyof TMap & string
25
+ ): Promise<string | undefined> | string | undefined
26
+ get(name: string): Promise<string | undefined> | string | undefined
27
+ get(name: string): Promise<string | undefined> | string | undefined {
28
+ return this.variables.get(name)
29
+ }
30
+
31
+ getJSON<K extends keyof TMap & string>(
32
+ name: K
33
+ ): Promise<TMap[K] | undefined> | TMap[K] | undefined
34
+ getJSON<T = unknown>(name: string): Promise<T | undefined> | T | undefined
35
+ getJSON(name: string): Promise<unknown> | unknown {
36
+ return this.variables.getJSON(name)
37
+ }
38
+
39
+ getAll():
40
+ | Promise<Record<string, string | undefined>>
41
+ | Record<string, string | undefined> {
42
+ return this.variables.getAll()
43
+ }
44
+
45
+ set(name: string, value: string): Promise<void> | void {
46
+ return this.variables.set(name, value)
47
+ }
48
+
49
+ setJSON(name: string, value: unknown): Promise<void> | void {
50
+ return this.variables.setJSON(name, value)
51
+ }
52
+
53
+ has(name: string): Promise<boolean> | boolean {
54
+ return this.variables.has(name)
55
+ }
56
+
57
+ delete(name: string): Promise<void> | void {
58
+ return this.variables.delete(name)
59
+ }
60
+
61
+ async getAllStatus(): Promise<VariableStatus[]> {
62
+ const results: VariableStatus[] = []
63
+ const all = await this.variables.getAll()
64
+
65
+ for (const [variableId, meta] of Object.entries(this.variablesMeta)) {
66
+ results.push({
67
+ variableId,
68
+ name: meta.name,
69
+ displayName: meta.displayName,
70
+ isConfigured: all[variableId] !== undefined,
71
+ })
72
+ }
73
+
74
+ return results
75
+ }
76
+
77
+ async getMissing(): Promise<VariableStatus[]> {
78
+ const all = await this.getAllStatus()
79
+ return all.filter((v) => !v.isConfigured)
80
+ }
81
+ }
@@ -0,0 +1,113 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import {
4
+ PikkuSessionService,
5
+ createMiddlewareSessionWireProps,
6
+ createFunctionSessionWireProps,
7
+ } from './user-session-service.js'
8
+
9
+ describe('PikkuSessionService', () => {
10
+ test('should set and get session', () => {
11
+ const service = new PikkuSessionService()
12
+ service.setInitial({ userId: 'user-1' })
13
+ const session = service.get()
14
+ assert.deepStrictEqual(session, { userId: 'user-1' })
15
+ })
16
+
17
+ test('should return undefined when no session set', () => {
18
+ const service = new PikkuSessionService()
19
+ assert.strictEqual(service.get(), undefined)
20
+ })
21
+
22
+ test('should mark sessionChanged when set is called', () => {
23
+ const service = new PikkuSessionService()
24
+ assert.strictEqual(service.sessionChanged, false)
25
+ service.set({ userId: 'user-1' })
26
+ assert.strictEqual(service.sessionChanged, true)
27
+ })
28
+
29
+ test('should clear session', () => {
30
+ const service = new PikkuSessionService()
31
+ service.setInitial({ userId: 'user-1' })
32
+ service.clear()
33
+ assert.strictEqual(service.get(), undefined)
34
+ assert.strictEqual(service.sessionChanged, true)
35
+ })
36
+
37
+ test('should freeze initial session', () => {
38
+ const service = new PikkuSessionService()
39
+ service.setInitial({ userId: 'user-1' })
40
+ const frozen = service.freezeInitial()
41
+ assert.deepStrictEqual(frozen, { userId: 'user-1' })
42
+ })
43
+
44
+ test('should only freeze initial once', () => {
45
+ const service = new PikkuSessionService()
46
+ service.setInitial({ userId: 'user-1' })
47
+ service.freezeInitial()
48
+ service.set({ userId: 'user-2' })
49
+ const frozen2 = service.freezeInitial()
50
+ assert.deepStrictEqual(frozen2, { userId: 'user-1' })
51
+ })
52
+
53
+ test('should throw when channelStore provided without channelId', () => {
54
+ assert.throws(() => new PikkuSessionService({} as any), {
55
+ message: 'Channel ID is required when using channel store',
56
+ })
57
+ })
58
+
59
+ test('should use channelStore when provided', async () => {
60
+ let storedSession: any
61
+ const store = {
62
+ setUserSession: (id: string, session: any) => {
63
+ storedSession = session
64
+ },
65
+ getChannelAndSession: (id: string) => ({ session: { userId: 'stored' } }),
66
+ }
67
+ const service = new PikkuSessionService(store as any, 'ch-1')
68
+ service.set({ userId: 'new' })
69
+ assert.deepStrictEqual(storedSession, { userId: 'new' })
70
+
71
+ const session = service.get()
72
+ assert.deepStrictEqual(session, { userId: 'stored' })
73
+ })
74
+ })
75
+
76
+ describe('createMiddlewareSessionWireProps', () => {
77
+ test('should return session wire props', () => {
78
+ const service = new PikkuSessionService()
79
+ service.setInitial({ userId: 'user-1' })
80
+ const props = createMiddlewareSessionWireProps(service)
81
+ assert.ok('session' in props)
82
+ assert.ok('setSession' in props)
83
+ assert.ok('getSession' in props)
84
+ assert.ok('hasSessionChanged' in props)
85
+ })
86
+
87
+ test('setSession should call setInitial', () => {
88
+ const service = new PikkuSessionService()
89
+ const props = createMiddlewareSessionWireProps(service)
90
+ props.setSession({ userId: 'user-2' })
91
+ assert.deepStrictEqual(service.get(), { userId: 'user-2' })
92
+ })
93
+ })
94
+
95
+ describe('createFunctionSessionWireProps', () => {
96
+ test('should return function session wire props', () => {
97
+ const service = new PikkuSessionService()
98
+ service.setInitial({ userId: 'user-1' })
99
+ const props = createFunctionSessionWireProps(service)
100
+ assert.ok('session' in props)
101
+ assert.ok('setSession' in props)
102
+ assert.ok('clearSession' in props)
103
+ assert.ok('getSession' in props)
104
+ assert.ok('hasSessionChanged' in props)
105
+ })
106
+
107
+ test('session should be frozen initial', () => {
108
+ const service = new PikkuSessionService()
109
+ service.setInitial({ userId: 'user-1' })
110
+ const props = createFunctionSessionWireProps(service)
111
+ assert.deepStrictEqual(props.session, { userId: 'user-1' })
112
+ })
113
+ })