@pikku/skills 0.12.22 → 0.12.26

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 (106) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/dist/skills.gen.js +1 -1
  3. package/package.json +1 -1
  4. package/skills/pikku-a11y/SKILL.md +59 -0
  5. package/skills/pikku-addon/SKILL.md +2 -2
  6. package/skills/pikku-agent/SKILL.md +67 -316
  7. package/skills/pikku-agent/references/agents.md +299 -0
  8. package/skills/{pikku-ai-vercel/SKILL.md → pikku-agent/references/runner-vercel.md} +2 -24
  9. package/skills/{pikku-ai-voice/SKILL.md → pikku-agent/references/voice.md} +1 -22
  10. package/skills/pikku-architect/SKILL.md +265 -0
  11. package/skills/pikku-auth/SKILL.md +89 -0
  12. package/skills/{pikku-better-auth/SKILL.md → pikku-auth/references/better-auth.md} +126 -34
  13. package/skills/{pikku-jose/SKILL.md → pikku-auth/references/jose.md} +1 -23
  14. package/skills/{pikku-machine-auth/SKILL.md → pikku-auth/references/machine-auth.md} +0 -23
  15. package/skills/pikku-auth/references/permissions.md +261 -0
  16. package/skills/{pikku-security/SKILL.md → pikku-auth/references/sessions.md} +0 -20
  17. package/skills/pikku-build/SKILL.md +88 -0
  18. package/skills/{pikku-build-app/SKILL.md → pikku-build/references/app.md} +76 -24
  19. package/skills/{pikku-feature/SKILL.md → pikku-build/references/feature.md} +1 -8
  20. package/skills/{pikku-build-app → pikku-build}/references/multi-app.md +56 -1
  21. package/skills/{pikku-build-platform/SKILL.md → pikku-build/references/platform.md} +22 -37
  22. package/skills/{pikku-template-clone/SKILL.md → pikku-build/references/post-clone.md} +0 -6
  23. package/skills/{pikku-build-quick/SKILL.md → pikku-build/references/quick.md} +4 -20
  24. package/skills/{pikku-build-app → pikku-build}/references/ship.md +9 -3
  25. package/skills/pikku-concepts/SKILL.md +72 -7
  26. package/skills/pikku-concepts/references/concept-mapping.md +8 -8
  27. package/skills/pikku-deploy/SKILL.md +158 -0
  28. package/skills/{pikku-deploy-azure/SKILL.md → pikku-deploy/references/azure.md} +18 -50
  29. package/skills/pikku-deploy/references/cloudflare.md +104 -0
  30. package/skills/pikku-deploy/references/express.md +92 -0
  31. package/skills/{pikku-deploy-fastify/SKILL.md → pikku-deploy/references/fastify.md} +8 -32
  32. package/skills/{pikku-deploy-lambda/SKILL.md → pikku-deploy/references/lambda.md} +6 -27
  33. package/skills/{pikku-deploy-nextjs/SKILL.md → pikku-deploy/references/nextjs.md} +9 -33
  34. package/skills/pikku-deploy/references/uws.md +72 -0
  35. package/skills/pikku-deploy/references/ws.md +75 -0
  36. package/skills/pikku-emails/SKILL.md +3 -2
  37. package/skills/pikku-fabric/SKILL.md +47 -20
  38. package/skills/{pikku-fabric-debug/SKILL.md → pikku-fabric/references/debugging.md} +0 -6
  39. package/skills/pikku-i18n/SKILL.md +62 -207
  40. package/skills/{pikku-paraglide/SKILL.md → pikku-i18n/references/enum-labels.md} +0 -6
  41. package/skills/pikku-i18n/references/messages.md +218 -0
  42. package/skills/{pikku-rtl/SKILL.md → pikku-i18n/references/rtl.md} +3 -9
  43. package/skills/pikku-knowledge/SKILL.md +15 -0
  44. package/skills/pikku-kysely/SKILL.md +13 -13
  45. package/skills/pikku-list-query/SKILL.md +163 -0
  46. package/skills/pikku-meta/SKILL.md +58 -130
  47. package/skills/{pikku-deps/SKILL.md → pikku-meta/references/audit.md} +1 -17
  48. package/skills/pikku-meta/references/meta.md +114 -0
  49. package/skills/{pikku-versioning/SKILL.md → pikku-meta/references/versioning.md} +0 -26
  50. package/skills/pikku-middleware/SKILL.md +5 -5
  51. package/skills/pikku-n8n-import/SKILL.md +0 -1
  52. package/skills/pikku-permissions/SKILL.md +75 -229
  53. package/skills/pikku-react/SKILL.md +50 -298
  54. package/skills/pikku-react/references/client.md +313 -0
  55. package/skills/{pikku-react-query/SKILL.md → pikku-react/references/react-query.md} +2 -22
  56. package/skills/{pikku-workflows-client/SKILL.md → pikku-react/references/workflows.md} +1 -22
  57. package/skills/pikku-realtime/SKILL.md +110 -251
  58. package/skills/pikku-scenario/SKILL.md +60 -45
  59. package/skills/pikku-scenario/references/persona-run.md +148 -0
  60. package/skills/pikku-seo/SKILL.md +133 -0
  61. package/skills/pikku-service-backends/SKILL.md +154 -0
  62. package/skills/pikku-service-backends/references/aws.md +106 -0
  63. package/skills/pikku-service-backends/references/backblaze.md +57 -0
  64. package/skills/pikku-service-backends/references/mongodb.md +90 -0
  65. package/skills/pikku-service-backends/references/redis.md +75 -0
  66. package/skills/pikku-service-backends/references/schema.md +63 -0
  67. package/skills/pikku-services/SKILL.md +68 -291
  68. package/skills/{pikku-audit/SKILL.md → pikku-services/references/audit.md} +0 -22
  69. package/skills/{pikku-config/SKILL.md → pikku-services/references/config.md} +1 -25
  70. package/skills/{pikku-pino/SKILL.md → pikku-services/references/pino.md} +0 -20
  71. package/skills/pikku-services/references/services.md +272 -0
  72. package/skills/pikku-software-archaeology/README.md +5 -1
  73. package/skills/pikku-software-archaeology/SKILL.md +16 -2
  74. package/skills/{pikku-product-second-opinion/example/sample-report.md → pikku-software-archaeology/example/second-opinion-sample-report.md} +1 -1
  75. package/skills/pikku-software-archaeology/references/blueprint.schema.json +1 -1
  76. package/skills/pikku-software-archaeology/references/pikku-mapping.md +3 -3
  77. package/skills/{pikku-product-second-opinion/SKILL.md → pikku-software-archaeology/references/second-opinion.md} +4 -9
  78. package/skills/pikku-webhook/SKILL.md +224 -0
  79. package/skills/pikku-wiring/SKILL.md +180 -0
  80. package/skills/{pikku-websocket/SKILL.md → pikku-wiring/references/channel.md} +2 -35
  81. package/skills/{pikku-cli/SKILL.md → pikku-wiring/references/cli.md} +1 -33
  82. package/skills/{pikku-gateway-slack/SKILL.md → pikku-wiring/references/gateway-slack.md} +0 -23
  83. package/skills/{pikku-http/SKILL.md → pikku-wiring/references/http.md} +3 -39
  84. package/skills/{pikku-mcp/SKILL.md → pikku-wiring/references/mcp.md} +0 -33
  85. package/skills/{pikku-queue/SKILL.md → pikku-wiring/references/queue.md} +1 -33
  86. package/skills/pikku-wiring/references/realtime.md +265 -0
  87. package/skills/{pikku-rpc/SKILL.md → pikku-wiring/references/rpc.md} +0 -32
  88. package/skills/{pikku-schedule/SKILL.md → pikku-wiring/references/scheduler.md} +1 -35
  89. package/skills/{pikku-trigger/SKILL.md → pikku-wiring/references/trigger.md} +0 -43
  90. package/skills/pikku-workflow/SKILL.md +39 -2
  91. package/skills/pikku-aws/SKILL.md +0 -161
  92. package/skills/pikku-backblaze/SKILL.md +0 -104
  93. package/skills/pikku-deploy-cloudflare/SKILL.md +0 -123
  94. package/skills/pikku-deploy-express/SKILL.md +0 -122
  95. package/skills/pikku-deploy-uws/SKILL.md +0 -144
  96. package/skills/pikku-mongodb/SKILL.md +0 -113
  97. package/skills/pikku-product-second-opinion/README.md +0 -43
  98. package/skills/pikku-redis/SKILL.md +0 -99
  99. package/skills/pikku-schema-ajv/SKILL.md +0 -83
  100. package/skills/pikku-schema-cfworker/SKILL.md +0 -82
  101. package/skills/pikku-ws/SKILL.md +0 -87
  102. /package/skills/{pikku-build-app → pikku-build}/references/theming.md +0 -0
  103. /package/skills/{pikku-product-second-opinion/references/report-template.md → pikku-software-archaeology/references/second-opinion-report-template.md} +0 -0
  104. /package/skills/{pikku-cli/references/complete-example.md → pikku-wiring/references/cli-complete-example.md} +0 -0
  105. /package/skills/{pikku-http → pikku-wiring}/references/http-options.md +0 -0
  106. /package/skills/{pikku-realtime/references/other-routes.md → pikku-wiring/references/realtime-other-routes.md} +0 -0
@@ -0,0 +1,106 @@
1
+ # AWS (`@pikku/aws-services`)
2
+
3
+ ```bash
4
+ yarn add @pikku/aws-services
5
+ ```
6
+
7
+ AWS-backed implementations of the content, queue, and secret interfaces.
8
+
9
+ ## `S3Content` — ContentService
10
+
11
+ ```typescript
12
+ import { S3Content } from '@pikku/aws-services'
13
+
14
+ const content = new S3Content(
15
+ config: { bucketName: string; region: string; endpoint?: string },
16
+ logger: Logger,
17
+ signConfig: { keyPairId: string; privateKey: string }
18
+ )
19
+ ```
20
+
21
+ `endpoint` is what points the client at LocalStack or an S3-compatible store.
22
+
23
+ Every method takes a single **args object**, matching the shared `ContentService`
24
+ interface. None of them are positional:
25
+
26
+ - `signURL({ url, dateLessThan, dateGreaterThan? }): Promise<string>` — CloudFront-sign an absolute URL
27
+ - `signContentKey({ bucket, contentKey, dateLessThan, dateGreaterThan? }): Promise<string>`
28
+ - `getUploadURL({ bucket, fileKey, contentType, visibility? }): Promise<{ uploadUrl, assetKey }>` — `visibility` is ignored
29
+ - `readFile({ bucket, key }): Promise<ReadableStream | NodeJS.ReadableStream>`
30
+ - `readFileAsBuffer({ bucket, key }): Promise<Buffer>`
31
+ - `writeFile({ bucket, key, stream }): Promise<boolean>`
32
+ - `copyFile({ bucket, key, fromAbsolutePath }): Promise<boolean>`
33
+ - `deleteFile({ bucket, key }): Promise<boolean>`
34
+
35
+ `signContentKey` builds `https://<bucketName>/<bucket>/<contentKey>` — it uses
36
+ `bucketName` as the **host**. For signed content that value must therefore be
37
+ your CloudFront domain, not a plain bucket name, which means the same config
38
+ field is doing two jobs.
39
+
40
+ Presigned upload URLs expire after a fixed **3600s**, not configurable through
41
+ the service.
42
+
43
+ ```typescript
44
+ const createSingletonServices = pikkuServices(async (config) => {
45
+ const logger = new PinoLogger()
46
+ const content = new S3Content(
47
+ { bucketName: config.s3Bucket, region: config.awsRegion },
48
+ logger,
49
+ { keyPairId: config.cfKeyPairId, privateKey: config.cfPrivateKey }
50
+ )
51
+ return { config, logger, content }
52
+ })
53
+ ```
54
+
55
+ ## `SQSQueueService` — QueueService
56
+
57
+ ```typescript
58
+ import { SQSQueueService } from '@pikku/aws-services'
59
+
60
+ const queue = new SQSQueueService({
61
+ region: string,
62
+ queueUrlPrefix: string, // e.g. 'https://sqs.us-east-1.amazonaws.com/123456789/'
63
+ endpoint?: string, // LocalStack or a custom SQS endpoint
64
+ })
65
+ ```
66
+
67
+ - `add<T>(queueName: string, data: T, options?: JobOptions): Promise<string>` — returns SQS's `MessageId`
68
+ - `getJob()` — always **throws**
69
+
70
+ The queue URL is `queueUrlPrefix + queueName`, so the name in `wireQueueWorker`
71
+ has to match the SQS queue exactly.
72
+
73
+ Constraints inherited from SQS, enforced in `add`:
74
+
75
+ - `options.delay` is in **milliseconds**, floored to whole seconds. Over
76
+ 900_000ms (15 minutes) or negative throws before the message is sent.
77
+ - Standard queues only — no FIFO, so no `MessageGroupId` and no ordering
78
+ guarantee.
79
+ - `data` is `JSON.stringify`d, which is where a `Date` or a `Map` quietly
80
+ degrades.
81
+
82
+ ```typescript
83
+ const createSingletonServices = pikkuServices(async (config) => {
84
+ const queue = new SQSQueueService({
85
+ region: config.awsRegion,
86
+ queueUrlPrefix: config.sqsUrlPrefix,
87
+ })
88
+ return { config, queue }
89
+ })
90
+ ```
91
+
92
+ ## `AWSSecrets` — SecretService
93
+
94
+ ```typescript
95
+ import { AWSSecrets } from '@pikku/aws-services'
96
+
97
+ const secrets = new AWSSecrets({ awsRegion: 'eu-west-2' })
98
+ ```
99
+
100
+ `AWSConfig` has one field, `awsRegion` — there is no credentials option; the
101
+ SDK's default provider chain (instance role, env, profile) supplies those.
102
+
103
+ - `getSecret<T = string>(SecretId: string): Promise<SecretValue<T>>` — a JSON secret is parsed automatically, so pass a shape as `T` (a non-JSON value comes back as the raw string). The result is a branded `SecretValue`, not a bare value — reveal it where it is used rather than passing it through logs
104
+ - `getSecrets<T>(SecretIds: (keyof T & string)[]): Promise<Partial<T>>` — missing keys are omitted rather than thrown
105
+ - `hasSecret(SecretId: string): Promise<boolean>` — performs a full fetch
106
+ - `setSecret` / `deleteSecret` — **not implemented**; they throw
@@ -0,0 +1,57 @@
1
+ # Backblaze B2 (`@pikku/backblaze`)
2
+
3
+ ```bash
4
+ yarn add @pikku/backblaze
5
+ ```
6
+
7
+ `B2Content` implements `ContentService` over Backblaze B2.
8
+
9
+ ```typescript
10
+ import { B2Content } from '@pikku/backblaze'
11
+
12
+ const content = new B2Content(config: B2ContentConfig, logger: Logger)
13
+ ```
14
+
15
+ `B2ContentConfig` has exactly three fields — `applicationKeyId`, `applicationKey`
16
+ and `bucketId`. There is no `cdnUrl`: downloads are served from the `downloadUrl`
17
+ B2 returns at authorization.
18
+
19
+ Every method takes a single **args object**, matching the shared `ContentService`
20
+ interface. None of them are positional:
21
+
22
+ - `signContentKey({ bucket, contentKey, dateLessThan }): Promise<string>` — a full download URL with an `Authorization` query param
23
+ - `signURL({ url, dateLessThan }): Promise<string>` — re-signs an existing `/file/` URL; a URL with no `/file/` segment is returned untouched
24
+ - `getUploadURL({ bucket, fileKey, contentType, visibility? }): Promise<UploadURLResult>` — `visibility` is ignored
25
+ - `writeFile({ bucket, key, stream }): Promise<boolean>`
26
+ - `copyFile({ bucket, key, fromAbsolutePath }): Promise<boolean>`
27
+ - `readFile({ bucket, key }): Promise<ReadableStream | NodeJS.ReadableStream>`
28
+ - `readFileAsBuffer({ bucket, key }): Promise<Buffer>`
29
+ - `deleteFile({ bucket, key }): Promise<boolean>`
30
+
31
+ Because `writeFile` drains the whole stream into a `Buffer` before uploading
32
+ (B2's upload endpoint needs a SHA-1 and a content length up front), large uploads
33
+ should go through `getUploadURL` and be sent by the client directly.
34
+
35
+ ```typescript
36
+ const createSingletonServices = pikkuServices(async (config) => {
37
+ const logger = new PinoLogger()
38
+ const content = new B2Content(
39
+ {
40
+ applicationKeyId: config.b2KeyId,
41
+ applicationKey: config.b2AppKey,
42
+ bucketId: config.b2BucketId,
43
+ },
44
+ logger
45
+ )
46
+ return { config, logger, content }
47
+ })
48
+ ```
49
+
50
+ ```typescript
51
+ await content.writeFile({ bucket: 'avatars', key: `${userId}.png`, stream })
52
+ const url = await content.signContentKey({
53
+ bucket: 'avatars',
54
+ contentKey: `${userId}.png`,
55
+ dateLessThan: new Date(Date.now() + 60_000),
56
+ })
57
+ ```
@@ -0,0 +1,90 @@
1
+ # MongoDB (`@pikku/mongodb`)
2
+
3
+ ```bash
4
+ yarn add @pikku/mongodb
5
+ ```
6
+
7
+ ## `PikkuMongoDB` — connection wrapper
8
+
9
+ Every service below takes a `Db`, so the wrapper is constructed and initialised
10
+ first.
11
+
12
+ ```typescript
13
+ import { PikkuMongoDB } from '@pikku/mongodb'
14
+
15
+ const mongo = new PikkuMongoDB(
16
+ logger: Logger,
17
+ clientOrUri: MongoClient | string,
18
+ dbName: string,
19
+ options?: MongoClientOptions
20
+ )
21
+
22
+ await mongo.init()
23
+ mongo.db // Db instance for queries
24
+ await mongo.close()
25
+ ```
26
+
27
+ ## Available services
28
+
29
+ | Service | Interface | Purpose |
30
+ | --- | --- | --- |
31
+ | `MongoDBChannelStore` | `ChannelStore` | WebSocket channel state persistence |
32
+ | `MongoDBEventHubStore` | `EventHubStore` | Event hub state persistence |
33
+ | `MongoDBWorkflowService` | `PikkuWorkflowService` | Workflow definition storage |
34
+ | `MongoDBWorkflowRunService` | `WorkflowRunService` | Workflow execution tracking |
35
+ | `MongoDBDeploymentService` | `DeploymentService` | Deployment state management |
36
+ | `MongoDBAgentStorageService` | `AgentStorageService`, `AgentRunStateService` | AI conversation/run storage |
37
+ | `MongoDBAgentRunService` | `AgentRunService` | Agent execution tracking |
38
+ | `MongoDBSecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |
39
+ | `MongoDBSessionStore` | `SessionStore` | Persisted user sessions |
40
+
41
+ All of them take a `Db` in the constructor and have an `init()` method that
42
+ creates the collections and indexes. **Await it** — a service used without it
43
+ behaves like an unindexed collection.
44
+
45
+ ## `MongoDBSecretService`
46
+
47
+ Envelope encryption: `key` derives the KEK that wraps each secret's own DEK.
48
+ Keeping `previousKey` set is what makes `rotateKEK()` possible — it re-wraps
49
+ every secret onto the current key and returns the new version.
50
+
51
+ ```typescript
52
+ import { MongoDBSecretService } from '@pikku/mongodb'
53
+
54
+ const secrets = new MongoDBSecretService(mongo.db, {
55
+ key: 'your-key-encryption-passphrase',
56
+ keyVersion: 2, // defaults to 1
57
+ previousKey: 'the-passphrase-you-are-rotating-away-from',
58
+ audit: true, // log write/delete/rotate through the audit sink
59
+ auditReads: false, // reads too — noisy, off by default
60
+ })
61
+ await secrets.init()
62
+
63
+ await secrets.setSecret('api-key', { key: 'sk-...' })
64
+ const value = await secrets.getSecret<{ key: string }>('api-key')
65
+ await secrets.rotateKEK()
66
+ ```
67
+
68
+ ## Full setup
69
+
70
+ ```typescript
71
+ import {
72
+ PikkuMongoDB,
73
+ MongoDBChannelStore,
74
+ MongoDBWorkflowService,
75
+ } from '@pikku/mongodb'
76
+
77
+ const createSingletonServices = pikkuServices(async (config) => {
78
+ const logger = new PinoLogger()
79
+ const mongo = new PikkuMongoDB(logger, config.mongoUri, 'myapp')
80
+ await mongo.init()
81
+
82
+ const channelStore = new MongoDBChannelStore(mongo.db)
83
+ await channelStore.init()
84
+
85
+ const workflowService = new MongoDBWorkflowService(mongo.db)
86
+ await workflowService.init()
87
+
88
+ return { config, logger, database: mongo, channelStore, workflowService }
89
+ })
90
+ ```
@@ -0,0 +1,75 @@
1
+ # Redis (`@pikku/redis`)
2
+
3
+ ```bash
4
+ yarn add @pikku/redis
5
+ ```
6
+
7
+ Redis-backed implementations of Pikku's core service interfaces, using
8
+ [ioredis](https://github.com/redis/ioredis). Every service accepts a Redis
9
+ connection — an ioredis `Redis` instance, `RedisOptions`, or a connection
10
+ string — in its constructor. None of them need an `init()` call.
11
+
12
+ | Service | Interface | Purpose |
13
+ | --- | --- | --- |
14
+ | `RedisChannelStore` | `ChannelStore` | WebSocket channel state persistence |
15
+ | `RedisEventHubStore` | `EventHubStore` | Event hub state persistence |
16
+ | `RedisWorkflowService` | `PikkuWorkflowService` | Workflow definition storage |
17
+ | `RedisWorkflowRunService` | `WorkflowRunService` | Workflow execution tracking |
18
+ | `RedisDeploymentService` | `DeploymentService` | Deployment state management |
19
+ | `RedisAgentRunService` | `AgentRunService` | Agent execution tracking |
20
+ | `RedisSecretService` | `SecretService` | Encrypted secret storage (envelope encryption) |
21
+ | `RedisSessionStore` | `SessionStore` | Persisted user sessions |
22
+
23
+ There is no Redis implementation of `AgentStorageService` — AI conversation
24
+ storage is MongoDB-only.
25
+
26
+ ## `RedisSecretService`
27
+
28
+ Envelope encryption: `key` derives the KEK that wraps each secret's own DEK.
29
+ Keeping `previousKey` set is what makes `rotateKEK()` possible — it re-wraps
30
+ every secret onto the current key and returns the new version.
31
+
32
+ ```typescript
33
+ import { RedisSecretService } from '@pikku/redis'
34
+
35
+ const secrets = new RedisSecretService(
36
+ connectionOrConfig: Redis | RedisOptions | string,
37
+ config: {
38
+ key: string // the KEK passphrase
39
+ keyVersion?: number // defaults to 1
40
+ previousKey?: string // required to rotate
41
+ keyPrefix?: string // namespaces the redis keys
42
+ }
43
+ )
44
+
45
+ await secrets.getSecret<T = string>(key: string): Promise<T>
46
+ await secrets.getSecrets<T>(keys: (keyof T & string)[]): Promise<Partial<T>>
47
+ await secrets.hasSecret(key: string): Promise<boolean>
48
+ await secrets.setSecret(key: string, value: unknown): Promise<void>
49
+ await secrets.deleteSecret(key: string): Promise<void>
50
+ await secrets.rotateKEK(): Promise<number>
51
+ await secrets.close(): Promise<void>
52
+ ```
53
+
54
+ ## Full setup
55
+
56
+ ```typescript
57
+ import {
58
+ RedisChannelStore,
59
+ RedisWorkflowService,
60
+ RedisSecretService,
61
+ } from '@pikku/redis'
62
+
63
+ const createSingletonServices = pikkuServices(async (config) => {
64
+ const logger = new PinoLogger()
65
+
66
+ const channelStore = new RedisChannelStore(config.redisUrl)
67
+ const workflowService = new RedisWorkflowService(config.redisUrl)
68
+
69
+ const secrets = new RedisSecretService(config.redisUrl, {
70
+ key: config.kekPassphrase,
71
+ })
72
+
73
+ return { config, logger, channelStore, workflowService, secrets }
74
+ })
75
+ ```
@@ -0,0 +1,63 @@
1
+ # Schema validation (`@pikku/schema-ajv`, `@pikku/schema-cfworker`)
2
+
3
+ Two implementations of `SchemaService` from `@pikku/core`. Pikku uses whichever
4
+ one is wired to validate function inputs and outputs against the schemas codegen
5
+ derives from your function definitions.
6
+
7
+ ```bash
8
+ yarn add @pikku/schema-ajv # default for Node.js
9
+ yarn add @pikku/schema-cfworker # Cloudflare Workers
10
+ ```
11
+
12
+ Both expose the same four methods:
13
+
14
+ - `compileSchema(name: string, schema: any): void` — compile and register under `name`
15
+ - `validateSchema(schemaName: string, json: any): void` — throws on failure
16
+ - `getSchemaNames(): Set<string>`
17
+ - `getSchemaKeys(schemaName: string): string[]` — top-level property keys, or `[]` if the schema has no `properties`
18
+
19
+ On `compileSchema` the first argument is the **name** and the second the schema —
20
+ the parameter is called `schema` in the source, which reads backwards.
21
+
22
+ ## `AjvSchemaService`
23
+
24
+ ```typescript
25
+ import { AjvSchemaService } from '@pikku/schema-ajv'
26
+
27
+ const schema = new AjvSchemaService(logger: Logger)
28
+ ```
29
+
30
+ Backed by [AJV](https://ajv.js.org/).
31
+
32
+ - **AJV is a module-level singleton**, shared by every `AjvSchemaService` you
33
+ construct, so compiled schema names are global to the process.
34
+ - **`useDefaults: true` mutates the validated object**, filling in schema
35
+ defaults in place.
36
+ - `ajv-formats` is registered, so `format` keywords (`email`, `uuid`,
37
+ `date-time`) are enforced.
38
+
39
+ ## `CFWorkerSchemaService`
40
+
41
+ ```typescript
42
+ import { CFWorkerSchemaService } from '@pikku/schema-cfworker'
43
+
44
+ const schema = new CFWorkerSchemaService(logger: Logger)
45
+ ```
46
+
47
+ Backed by [@cfworker/json-schema](https://github.com/cfworker/cfworker), which
48
+ uses no `eval` or `new Function` and so runs where AJV cannot.
49
+
50
+ - Each validator gets a **deep clone** of the schema (`@cfworker/json-schema`
51
+ mutates what it is given, which throws on a frozen generated object).
52
+ - A compile failure throws `Error('Failed to compile schema: <name>')` with the
53
+ underlying cause swallowed — check the schema by hand when you see it.
54
+
55
+ ## Wiring either one
56
+
57
+ ```typescript
58
+ const createSingletonServices = pikkuServices(async (config) => {
59
+ const logger = new ConsoleLogger()
60
+ const schema = new AjvSchemaService(logger)
61
+ return { config, logger, schema }
62
+ })
63
+ ```