@pikku/cli 0.12.90 → 0.12.92
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.
- package/LICENSE +106 -0
- package/README.md +25 -2
- package/console-app/assets/{index-CSzCJzBb.css → index-D0HG8q0B.css} +1 -1
- package/console-app/assets/{index-C5Bd44e4.js → index-DDpIMCpy.js} +149 -149
- package/console-app/index.html +2 -2
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +6 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.json +15 -0
- package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +43 -0
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +4 -4
- package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +39 -44
- package/dist/.pikku/function/pikku-functions.gen.js +1 -3
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap-scenarios.gen.d.ts +9 -0
- package/dist/.pikku/pikku-bootstrap-scenarios.gen.js +9 -0
- package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +1 -1
- package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.d.ts +1 -0
- package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.js +10 -0
- package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.json +1 -0
- package/dist/.pikku/scenarios/pikku-scenario-functions.gen.d.ts +4 -0
- package/dist/.pikku/scenarios/pikku-scenario-functions.gen.js +1 -0
- package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.d.ts +1 -0
- package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.js +10 -0
- package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.d.ts +4 -0
- package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.js +1 -0
- package/dist/.pikku/scenarios/schemas/register.gen.d.ts +4 -0
- package/dist/.pikku/scenarios/schemas/register.gen.js +4 -0
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +5 -3
- package/dist/.pikku/schemas/schemas/FabricSecretsListOutput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/FabricSecretsRotateInput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/FabricSecretsRotateOutput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/FabricSecretsSetOutput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/ScenarioRunInput.schema.json +1 -1
- package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +1 -1
- package/dist/.pikku/scopes/pikku-scope-types.gen.js +1 -1
- package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/meta/allWorkflow.gen.json +2 -8
- package/dist/.pikku/workflow/pikku-scenario-actors.gen.d.ts +19 -0
- package/dist/.pikku/workflow/pikku-scenario-actors.gen.js +17 -0
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +152 -3
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +39 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/bin/pikku-bin.mjs +2 -2
- package/dist/src/cli.wiring.js +28 -0
- package/dist/src/deploy/analyzer/analyzer.js +22 -5
- package/dist/src/deploy/build-pipeline.js +5 -1
- package/dist/src/fabric/fabric-commands.d.ts +37 -9
- package/dist/src/fabric/fabric-commands.js +12 -0
- package/dist/src/fabric/functions/domains-add.function.d.ts +4 -4
- package/dist/src/fabric/functions/secrets-list.function.d.ts +16 -4
- package/dist/src/fabric/functions/secrets-list.function.js +12 -10
- package/dist/src/fabric/functions/secrets-rotate.function.d.ts +24 -0
- package/dist/src/fabric/functions/secrets-rotate.function.js +36 -0
- package/dist/src/fabric/functions/secrets-set.function.d.ts +8 -4
- package/dist/src/fabric/functions/secrets-set.function.js +18 -7
- package/dist/src/fabric/lib/http.d.ts +7 -1
- package/dist/src/fabric/lib/sealed-box.d.ts +16 -0
- package/dist/src/fabric/lib/sealed-box.js +72 -0
- package/dist/src/functions/commands/dev.js +11 -0
- package/dist/src/functions/commands/load-user-project.d.ts +7 -0
- package/dist/src/functions/commands/load-user-project.js +21 -0
- package/dist/src/functions/commands/pikku-command-bootstrap.js +13 -0
- package/dist/src/functions/commands/scenario-browser.d.ts +84 -0
- package/dist/src/functions/commands/scenario-browser.js +68 -0
- package/dist/src/functions/commands/scenario-environment.d.ts +32 -0
- package/dist/src/functions/commands/scenario-environment.js +61 -0
- package/dist/src/functions/commands/scenario-formatter.d.ts +81 -0
- package/dist/src/functions/commands/scenario-formatter.js +120 -0
- package/dist/src/functions/commands/scenario-ladder.d.ts +66 -0
- package/dist/src/functions/commands/scenario-ladder.js +132 -0
- package/dist/src/functions/commands/scenario-plan.d.ts +61 -0
- package/dist/src/functions/commands/scenario-plan.js +106 -0
- package/dist/src/functions/commands/scenario.d.ts +24 -0
- package/dist/src/functions/commands/scenario.js +322 -50
- package/dist/src/functions/commands/serve.js +2 -0
- package/dist/src/functions/commands/skills.js +27 -61
- package/dist/src/functions/db/db-codegen.d.ts +6 -0
- package/dist/src/functions/db/db-codegen.js +9 -0
- package/dist/src/functions/db/db-migrator.js +14 -0
- package/dist/src/functions/db/local-db.d.ts +8 -0
- package/dist/src/functions/db/local-db.js +24 -2
- package/dist/src/functions/db/migration-identifiers.d.ts +58 -0
- package/dist/src/functions/db/migration-identifiers.js +262 -0
- package/dist/src/functions/db/migration-provenance.d.ts +35 -0
- package/dist/src/functions/db/migration-provenance.js +80 -0
- package/dist/src/functions/db/schema-sql.d.ts +43 -0
- package/dist/src/functions/db/schema-sql.js +135 -0
- package/dist/src/functions/db/sqlite/sqlite-kysely.js +22 -2
- package/dist/src/functions/wirings/functions/pikku-command-functions.js +26 -8
- package/dist/src/functions/wirings/functions/schemas.js +13 -1
- package/dist/src/functions/wirings/functions/serialize-function-types.js +3 -3
- package/dist/src/functions/wirings/rpc/pikku-command-rpc.js +6 -1
- package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.d.ts +10 -0
- package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.js +102 -0
- package/dist/src/functions/wirings/scenarios/scenario-partition.d.ts +42 -0
- package/dist/src/functions/wirings/scenarios/scenario-partition.js +71 -0
- package/dist/src/functions/wirings/scenarios/scenario-schema-partition.d.ts +33 -0
- package/dist/src/functions/wirings/scenarios/scenario-schema-partition.js +49 -0
- package/dist/src/functions/wirings/scenarios/serialize-feature-meta.d.ts +14 -0
- package/dist/src/functions/wirings/scenarios/serialize-feature-meta.js +29 -0
- package/dist/src/functions/wirings/scenarios/serialize-scenario-meta.d.ts +12 -0
- package/dist/src/functions/wirings/scenarios/serialize-scenario-meta.js +56 -0
- package/dist/src/functions/wirings/scenarios/serialize-scenario-registration.d.ts +7 -0
- package/dist/src/functions/wirings/scenarios/serialize-scenario-registration.js +40 -0
- package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +43 -8
- package/dist/src/functions/wirings/workflow/serialize-scenario-actors.d.ts +1 -1
- package/dist/src/functions/wirings/workflow/serialize-scenario-actors.js +13 -2
- package/dist/src/functions/wirings/workflow/serialize-scenario-step-map.d.ts +4 -0
- package/dist/src/functions/wirings/workflow/serialize-scenario-step-map.js +62 -0
- package/dist/src/functions/wirings/workflow/serialize-workflow-bootstrap-map.js +10 -3
- package/dist/src/functions/wirings/workflow/serialize-workflow-map.js +10 -3
- package/dist/src/functions/wirings/workflow/serialize-workflow-meta.js +4 -4
- package/dist/src/functions/wirings/workflow/serialize-workflow-types.d.ts +1 -1
- package/dist/src/functions/wirings/workflow/serialize-workflow-types.js +232 -3
- package/dist/src/functions/workflows/all.workflow.js +2 -7
- package/dist/src/server/server-ready.d.ts +12 -0
- package/dist/src/server/server-ready.js +12 -0
- package/dist/src/server/spawn-dev-server.d.ts +50 -0
- package/dist/src/server/spawn-dev-server.js +112 -0
- package/dist/src/services.js +12 -1
- package/dist/src/utils/file-writer.js +14 -2
- package/dist/src/utils/meta-diff.js +5 -2
- package/dist/src/utils/pikku-cli-config.d.ts +14 -0
- package/dist/src/utils/pikku-cli-config.js +60 -0
- package/dist/src/utils/remove-legacy-scaffold-file.d.ts +14 -0
- package/dist/src/utils/remove-legacy-scaffold-file.js +23 -0
- package/dist/src/utils/resolve-scenario-actors.d.ts +23 -0
- package/dist/src/utils/resolve-scenario-actors.js +74 -0
- package/dist/src/utils/serialize-schemas.d.ts +3 -1
- package/dist/src/utils/serialize-schemas.js +39 -4
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +18 -8
- package/dist/.pikku/schemas/schemas/PikkuScenarioFunctionsOutput.schema.json +0 -1
- package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.d.ts +0 -1
- package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.js +0 -31
- package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.d.ts +0 -10
- package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.js +0 -104
- package/skills/pikku-addon/SKILL.md +0 -243
- package/skills/pikku-addon/references/addon-package-manifest.md +0 -63
- package/skills/pikku-ai-agent/SKILL.md +0 -231
- package/skills/pikku-ai-vercel/SKILL.md +0 -82
- package/skills/pikku-ai-voice/SKILL.md +0 -88
- package/skills/pikku-audit/SKILL.md +0 -175
- package/skills/pikku-aws/SKILL.md +0 -111
- package/skills/pikku-backblaze/SKILL.md +0 -71
- package/skills/pikku-better-auth/SKILL.md +0 -298
- package/skills/pikku-cli/SKILL.md +0 -198
- package/skills/pikku-cli/references/complete-example.md +0 -82
- package/skills/pikku-concepts/SKILL.md +0 -250
- package/skills/pikku-concepts/references/concept-mapping.md +0 -556
- package/skills/pikku-concepts/references/packages.md +0 -29
- package/skills/pikku-config/SKILL.md +0 -212
- package/skills/pikku-cron/SKILL.md +0 -214
- package/skills/pikku-deploy-azure/SKILL.md +0 -71
- package/skills/pikku-deploy-cloudflare/SKILL.md +0 -82
- package/skills/pikku-deploy-express/SKILL.md +0 -86
- package/skills/pikku-deploy-fastify/SKILL.md +0 -73
- package/skills/pikku-deploy-lambda/SKILL.md +0 -110
- package/skills/pikku-deploy-nextjs/SKILL.md +0 -78
- package/skills/pikku-deploy-uws/SKILL.md +0 -88
- package/skills/pikku-deps/SKILL.md +0 -98
- package/skills/pikku-emails/SKILL.md +0 -163
- package/skills/pikku-fabric/SKILL.md +0 -318
- package/skills/pikku-fabric-debug/SKILL.md +0 -112
- package/skills/pikku-feature/SKILL.md +0 -258
- package/skills/pikku-gateway-slack/SKILL.md +0 -115
- package/skills/pikku-http/SKILL.md +0 -220
- package/skills/pikku-http/references/http-options.md +0 -55
- package/skills/pikku-i18n/SKILL.md +0 -137
- package/skills/pikku-info/SKILL.md +0 -100
- package/skills/pikku-jose/SKILL.md +0 -105
- package/skills/pikku-kysely/SKILL.md +0 -219
- package/skills/pikku-machine-auth/SKILL.md +0 -183
- package/skills/pikku-mcp/SKILL.md +0 -241
- package/skills/pikku-middleware/SKILL.md +0 -231
- package/skills/pikku-middleware/references/middleware-patterns.md +0 -61
- package/skills/pikku-mongodb/SKILL.md +0 -105
- package/skills/pikku-n8n-import/SKILL.md +0 -109
- package/skills/pikku-n8n-import/SPEC.md +0 -84
- package/skills/pikku-n8n-import/references/addon-mapping.md +0 -121
- package/skills/pikku-n8n-import/references/code-translation.md +0 -121
- package/skills/pikku-n8n-import/references/loops-and-control.md +0 -87
- package/skills/pikku-paraglide/SKILL.md +0 -117
- package/skills/pikku-permissions/SKILL.md +0 -192
- package/skills/pikku-pino/SKILL.md +0 -79
- package/skills/pikku-product-second-opinion/README.md +0 -43
- package/skills/pikku-product-second-opinion/SKILL.md +0 -158
- package/skills/pikku-product-second-opinion/example/sample-report.md +0 -90
- package/skills/pikku-product-second-opinion/references/report-template.md +0 -73
- package/skills/pikku-queue/SKILL.md +0 -240
- package/skills/pikku-react/SKILL.md +0 -212
- package/skills/pikku-react-query/SKILL.md +0 -242
- package/skills/pikku-realtime/SKILL.md +0 -236
- package/skills/pikku-realtime/references/other-routes.md +0 -23
- package/skills/pikku-redis/SKILL.md +0 -90
- package/skills/pikku-rpc/SKILL.md +0 -171
- package/skills/pikku-rtl/SKILL.md +0 -219
- package/skills/pikku-scenario/SKILL.md +0 -215
- package/skills/pikku-schedule/SKILL.md +0 -57
- package/skills/pikku-schema-ajv/SKILL.md +0 -62
- package/skills/pikku-schema-cfworker/SKILL.md +0 -63
- package/skills/pikku-security/SKILL.md +0 -108
- package/skills/pikku-services/SKILL.md +0 -248
- package/skills/pikku-services/references/audit-wire-service.md +0 -34
- package/skills/pikku-software-archaeology/README.md +0 -70
- package/skills/pikku-software-archaeology/SKILL.md +0 -186
- package/skills/pikku-software-archaeology/references/blueprint.schema.json +0 -625
- package/skills/pikku-software-archaeology/references/pikku-mapping.md +0 -49
- package/skills/pikku-software-archaeology/scripts/validate.mjs +0 -173
- package/skills/pikku-tag-middleware/SKILL.md +0 -13
- package/skills/pikku-template-clone/SKILL.md +0 -40
- package/skills/pikku-trigger/SKILL.md +0 -181
- package/skills/pikku-versioning/SKILL.md +0 -173
- package/skills/pikku-websocket/SKILL.md +0 -243
- package/skills/pikku-workflow/SKILL.md +0 -172
- package/skills/pikku-workflow/references/workflow-reference.md +0 -63
- package/skills/pikku-workflows-client/SKILL.md +0 -150
- package/skills/pikku-ws/SKILL.md +0 -47
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const FabricSecretsRotateInput: z.ZodObject<{
|
|
3
|
+
branch: z.ZodString;
|
|
4
|
+
force: z.ZodOptional<z.ZodBoolean>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export declare const FabricSecretsRotateOutput: z.ZodObject<{
|
|
7
|
+
retiredKeyId: z.ZodString;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
export declare const FabricSecretsRotate: import("../../../.pikku/pikku-types.gen.js").PikkuFunctionConfig<{
|
|
10
|
+
branch: string;
|
|
11
|
+
force?: boolean | undefined;
|
|
12
|
+
}, {
|
|
13
|
+
retiredKeyId: string;
|
|
14
|
+
}, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").PikkuFunctionSessionless<{
|
|
15
|
+
branch: string;
|
|
16
|
+
force?: boolean | undefined;
|
|
17
|
+
}, {
|
|
18
|
+
retiredKeyId: string;
|
|
19
|
+
}, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").WiredServices> | import("../../../.pikku/pikku-types.gen.js").PikkuFunction<{
|
|
20
|
+
branch: string;
|
|
21
|
+
force?: boolean | undefined;
|
|
22
|
+
}, {
|
|
23
|
+
retiredKeyId: string;
|
|
24
|
+
}, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").WiredServices>, undefined, undefined>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { pikkuSessionlessFunc } from '../../../.pikku/pikku-types.gen.js';
|
|
3
|
+
import { resolveApiContext } from '../lib/config.js';
|
|
4
|
+
import { getFabricRPC } from '../lib/http.js';
|
|
5
|
+
import { resolveStageId } from '../lib/stage.js';
|
|
6
|
+
export const FabricSecretsRotateInput = z.object({
|
|
7
|
+
branch: z.string(),
|
|
8
|
+
force: z.boolean().optional(),
|
|
9
|
+
});
|
|
10
|
+
export const FabricSecretsRotateOutput = z.object({
|
|
11
|
+
retiredKeyId: z.string(),
|
|
12
|
+
});
|
|
13
|
+
export const FabricSecretsRotate = pikkuSessionlessFunc({
|
|
14
|
+
description: "Retire a stage's sealing key so the next deploy issues a new one. Secrets sealed to the old key must be set again.",
|
|
15
|
+
input: FabricSecretsRotateInput,
|
|
16
|
+
output: FabricSecretsRotateOutput,
|
|
17
|
+
func: async (_services, { branch, force }) => {
|
|
18
|
+
const ctx = await resolveApiContext();
|
|
19
|
+
if (!ctx.token)
|
|
20
|
+
throw new Error('Not logged in. Run `pikku fabric login` first.');
|
|
21
|
+
if (!ctx.projectId)
|
|
22
|
+
throw new Error('No fabric project linked. Run `pikku fabric link` first.');
|
|
23
|
+
if (!force) {
|
|
24
|
+
// Not a confirmation for politeness — this is the one operation here
|
|
25
|
+
// that destroys access to data. Fabric cannot read the sealed values, so
|
|
26
|
+
// it cannot carry them over, and nobody can undo it afterwards.
|
|
27
|
+
throw new Error(`Rotating the sealing key on ${branch} makes every secret already set on it unreadable — fabric cannot re-seal values it cannot open, so each one must be set again after the next deploy. Re-run with --force if that is what you want.`);
|
|
28
|
+
}
|
|
29
|
+
const rpc = getFabricRPC({ apiUrl: ctx.apiUrl, token: ctx.token });
|
|
30
|
+
const stageId = await resolveStageId(rpc, ctx.projectId, branch);
|
|
31
|
+
const result = await rpc.invoke('rotateStageSealingKey', { stageId });
|
|
32
|
+
console.log(`[fabric] retired sealing key ${result.retiredKeyId} on ${branch}`);
|
|
33
|
+
console.log('[fabric] deploy the stage to issue a new key, then set its secrets again');
|
|
34
|
+
return result;
|
|
35
|
+
},
|
|
36
|
+
});
|
|
@@ -6,7 +6,8 @@ export declare const FabricSecretsSetInput: z.ZodObject<{
|
|
|
6
6
|
force: z.ZodOptional<z.ZodBoolean>;
|
|
7
7
|
}, z.core.$strip>;
|
|
8
8
|
export declare const FabricSecretsSetOutput: z.ZodObject<{
|
|
9
|
-
|
|
9
|
+
name: z.ZodString;
|
|
10
|
+
keyId: z.ZodString;
|
|
10
11
|
}, z.core.$strip>;
|
|
11
12
|
export declare const FabricSecretsSet: import("../../../.pikku/pikku-types.gen.js").PikkuFunctionConfig<{
|
|
12
13
|
name: string;
|
|
@@ -14,19 +15,22 @@ export declare const FabricSecretsSet: import("../../../.pikku/pikku-types.gen.j
|
|
|
14
15
|
value?: string | undefined;
|
|
15
16
|
force?: boolean | undefined;
|
|
16
17
|
}, {
|
|
17
|
-
|
|
18
|
+
name: string;
|
|
19
|
+
keyId: string;
|
|
18
20
|
}, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").PikkuFunctionSessionless<{
|
|
19
21
|
name: string;
|
|
20
22
|
branch: string;
|
|
21
23
|
value?: string | undefined;
|
|
22
24
|
force?: boolean | undefined;
|
|
23
25
|
}, {
|
|
24
|
-
|
|
26
|
+
name: string;
|
|
27
|
+
keyId: string;
|
|
25
28
|
}, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").WiredServices> | import("../../../.pikku/pikku-types.gen.js").PikkuFunction<{
|
|
26
29
|
name: string;
|
|
27
30
|
branch: string;
|
|
28
31
|
value?: string | undefined;
|
|
29
32
|
force?: boolean | undefined;
|
|
30
33
|
}, {
|
|
31
|
-
|
|
34
|
+
name: string;
|
|
35
|
+
keyId: string;
|
|
32
36
|
}, "rpc" | "session", import("../../../.pikku/pikku-types.gen.js").WiredServices>, undefined, undefined>;
|
|
@@ -3,15 +3,20 @@ import { pikkuSessionlessFunc } from '../../../.pikku/pikku-types.gen.js';
|
|
|
3
3
|
import { resolveApiContext } from '../lib/config.js';
|
|
4
4
|
import { getFabricRPC } from '../lib/http.js';
|
|
5
5
|
import { promptSecret } from '../lib/prompt.js';
|
|
6
|
+
import { resolveStageId } from '../lib/stage.js';
|
|
7
|
+
import { seal, serializeSealedValue } from '../lib/sealed-box.js';
|
|
6
8
|
export const FabricSecretsSetInput = z.object({
|
|
7
9
|
name: z.string(),
|
|
8
10
|
branch: z.string(),
|
|
9
11
|
value: z.string().optional(),
|
|
10
12
|
force: z.boolean().optional(),
|
|
11
13
|
});
|
|
12
|
-
export const FabricSecretsSetOutput = z.object({
|
|
14
|
+
export const FabricSecretsSetOutput = z.object({
|
|
15
|
+
name: z.string(),
|
|
16
|
+
keyId: z.string(),
|
|
17
|
+
});
|
|
13
18
|
export const FabricSecretsSet = pikkuSessionlessFunc({
|
|
14
|
-
description: 'Set a stage-scoped secret.
|
|
19
|
+
description: 'Set a stage-scoped secret. Sealed here to the stage public key — the plaintext never leaves this machine.',
|
|
15
20
|
input: FabricSecretsSetInput,
|
|
16
21
|
output: FabricSecretsSetOutput,
|
|
17
22
|
func: async (_services, { name, branch, value }) => {
|
|
@@ -24,13 +29,19 @@ export const FabricSecretsSet = pikkuSessionlessFunc({
|
|
|
24
29
|
if (!plaintext)
|
|
25
30
|
throw new Error('Empty secret value — aborting.');
|
|
26
31
|
const rpc = getFabricRPC({ apiUrl: ctx.apiUrl, token: ctx.token });
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
const stageId = await resolveStageId(rpc, ctx.projectId, branch);
|
|
33
|
+
// Fabric holds only the public half of this keypair. The private half went
|
|
34
|
+
// to the stage's worker at deploy time and was dropped there, so the value
|
|
35
|
+
// sealed below can be opened by that worker and by nothing else — including
|
|
36
|
+
// by the fabric process that stores it.
|
|
37
|
+
const key = await rpc.invoke('getStageSealingKey', { stageId });
|
|
38
|
+
const sealed = seal(key.publicKey, key.keyId, plaintext);
|
|
39
|
+
const result = await rpc.invoke('setStageSealedSecret', {
|
|
40
|
+
stageId,
|
|
30
41
|
name,
|
|
31
|
-
|
|
42
|
+
sealedValue: serializeSealedValue(sealed),
|
|
32
43
|
});
|
|
33
|
-
console.log(`[fabric] ${name} set on ${branch}.`);
|
|
44
|
+
console.log(`[fabric] ${name} sealed to ${result.keyId} and set on ${branch}.`);
|
|
34
45
|
return result;
|
|
35
46
|
},
|
|
36
47
|
});
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-safe RPC client backed by the snapshot SDK in `../sdk/`. The map is
|
|
3
|
+
* a copy of fabric's generated rpc-map at the time of the addon's last
|
|
4
|
+
* release — versioning bumps are taken by republishing this package.
|
|
5
|
+
*/
|
|
6
|
+
import { PikkuRPC } from '../sdk/pikku-rpc.gen.js';
|
|
1
7
|
export declare function getFabricRPC(opts: {
|
|
2
8
|
apiUrl: string;
|
|
3
9
|
token: string | null;
|
|
4
|
-
}):
|
|
10
|
+
}): PikkuRPC;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Algorithm tag carried by every sealed value. Must match fabric's. */
|
|
2
|
+
export declare const SEALED_BOX_ALGORITHM = "sealed-x25519-v1";
|
|
3
|
+
export interface SealedValue {
|
|
4
|
+
alg: typeof SEALED_BOX_ALGORITHM;
|
|
5
|
+
/** The `encryption_key` row whose public key sealed this. */
|
|
6
|
+
kid: string;
|
|
7
|
+
/** Ephemeral public key, SPKI DER as base64. */
|
|
8
|
+
epk: string;
|
|
9
|
+
iv: string;
|
|
10
|
+
ct: string;
|
|
11
|
+
tag: string;
|
|
12
|
+
}
|
|
13
|
+
/** Seal a value to a stage's public key. */
|
|
14
|
+
export declare function seal(publicKey: string, keyId: string, plaintext: string): SealedValue;
|
|
15
|
+
/** Wire form. Fabric stores exactly this string. */
|
|
16
|
+
export declare function serializeSealedValue(sealed: SealedValue): string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sealing a value to a stage's public key, here on the client.
|
|
3
|
+
*
|
|
4
|
+
* `pikku fabric secrets set` seals before it sends, so the plaintext never
|
|
5
|
+
* reaches fabric — not the ingress in front of it, not a request log, not the
|
|
6
|
+
* process handling the write. Fabric stores the sealed blob and hands it to the
|
|
7
|
+
* worker, which holds the only private key that opens it.
|
|
8
|
+
*
|
|
9
|
+
* This is the seal half of fabric's `sealed-x25519-v1`, and the two must stay
|
|
10
|
+
* byte-compatible: an ephemeral X25519 keypair, ECDH against the recipient,
|
|
11
|
+
* HKDF-SHA256 to a content key, AES-256-GCM to seal. Everything below is
|
|
12
|
+
* `node:crypto` on purpose — a sealing primitive that pulled in a dependency
|
|
13
|
+
* would be a supply-chain question sitting directly on the plaintext.
|
|
14
|
+
*
|
|
15
|
+
* There is deliberately no `unseal` here. The CLI has no private key and no
|
|
16
|
+
* business having one; opening a value happens in the worker.
|
|
17
|
+
*/
|
|
18
|
+
import { createCipheriv, createPublicKey, diffieHellman, generateKeyPairSync, hkdfSync, randomBytes, } from 'node:crypto';
|
|
19
|
+
/** Algorithm tag carried by every sealed value. Must match fabric's. */
|
|
20
|
+
export const SEALED_BOX_ALGORITHM = 'sealed-x25519-v1';
|
|
21
|
+
/** HKDF domain separator. Must match fabric's byte for byte. */
|
|
22
|
+
const SEAL_SALT = 'pikkufabric/sealed-box/v1';
|
|
23
|
+
/** Seal a value to a stage's public key. */
|
|
24
|
+
export function seal(publicKey, keyId, plaintext) {
|
|
25
|
+
const recipient = importPublicKey(publicKey);
|
|
26
|
+
const ephemeral = generateKeyPairSync('x25519');
|
|
27
|
+
const epk = ephemeral.publicKey.export({ type: 'spki', format: 'der' });
|
|
28
|
+
const contentKey = deriveContentKey(diffieHellman({ privateKey: ephemeral.privateKey, publicKey: recipient }), epk, recipient.export({ type: 'spki', format: 'der' }));
|
|
29
|
+
const iv = randomBytes(12);
|
|
30
|
+
const cipher = createCipheriv('aes-256-gcm', contentKey, iv);
|
|
31
|
+
const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
32
|
+
return {
|
|
33
|
+
alg: SEALED_BOX_ALGORITHM,
|
|
34
|
+
kid: keyId,
|
|
35
|
+
epk: epk.toString('base64'),
|
|
36
|
+
iv: iv.toString('base64'),
|
|
37
|
+
ct: ct.toString('base64'),
|
|
38
|
+
tag: cipher.getAuthTag().toString('base64'),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Wire form. Fabric stores exactly this string. */
|
|
42
|
+
export function serializeSealedValue(sealed) {
|
|
43
|
+
return JSON.stringify(sealed);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Both public keys go into the HKDF `info`, which is what stops a sealed value
|
|
47
|
+
* being redirected: change either the ephemeral key or the recipient and the
|
|
48
|
+
* derived content key changes with it.
|
|
49
|
+
*/
|
|
50
|
+
function deriveContentKey(shared, epk, recipientPublic) {
|
|
51
|
+
return Buffer.from(hkdfSync('sha256', shared, Buffer.from(SEAL_SALT), Buffer.concat([epk, recipientPublic]), 32));
|
|
52
|
+
}
|
|
53
|
+
function importPublicKey(publicKey) {
|
|
54
|
+
let key;
|
|
55
|
+
try {
|
|
56
|
+
key = createPublicKey({
|
|
57
|
+
key: Buffer.from(publicKey, 'base64'),
|
|
58
|
+
type: 'spki',
|
|
59
|
+
format: 'der',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
throw new Error(`Not a valid X25519 public key (${error instanceof Error ? error.message : String(error)})`);
|
|
64
|
+
}
|
|
65
|
+
// A public key of the wrong curve would still ECDH, just against a key the
|
|
66
|
+
// stage's worker cannot match — the failure would surface as an unopenable
|
|
67
|
+
// secret much later, so refuse it at the point the type is still knowable.
|
|
68
|
+
if (key.asymmetricKeyType !== 'x25519') {
|
|
69
|
+
throw new Error(`Stage sealing key is ${key.asymmetricKeyType}, expected x25519 — this CLI is too old for it, run \`npm i -g @pikku/cli\``);
|
|
70
|
+
}
|
|
71
|
+
return key;
|
|
72
|
+
}
|
|
@@ -11,9 +11,11 @@ import { LocalContent, } from '@pikku/core/services/local-content';
|
|
|
11
11
|
import { InMemorySchedulerService } from '@pikku/schedule';
|
|
12
12
|
import { resolveDb, createKysely, parseDatabaseUrl, } from '../db/local-db.js';
|
|
13
13
|
import { loadUserBootstrap, loadUserModule } from './load-user-project.js';
|
|
14
|
+
import { registerScenarioInstrumentation } from '../wirings/scenarios/register-scenario-instrumentation.js';
|
|
14
15
|
import { startCoverageService } from './start-coverage.js';
|
|
15
16
|
import { createDevAIAgentRunner } from './dev-ai-runner.js';
|
|
16
17
|
import { resolveConsoleMount } from './serve-console.js';
|
|
18
|
+
import { serverReadyLine } from '../../server/server-ready.js';
|
|
17
19
|
export const dev = pikkuSessionlessFunc({
|
|
18
20
|
remote: true,
|
|
19
21
|
func: async ({ logger, config, getInspectorState, invalidateInspectorState, variables, devServerRunner, }, { port, watch, hmr, coverage, test }, { rpc }) => {
|
|
@@ -114,6 +116,14 @@ export const dev = pikkuSessionlessFunc({
|
|
|
114
116
|
return;
|
|
115
117
|
}
|
|
116
118
|
await loadUserBootstrap(pikkuDir);
|
|
119
|
+
// Scenario instrumentation exists only on a development server: the runner
|
|
120
|
+
// calls it over RPC to reset and snapshot coverage and stub calls. It is
|
|
121
|
+
// registered here, after the app bootstrap, rather than generated into the
|
|
122
|
+
// project — an app bootstrap is what a deployed bundle imports, and coverage
|
|
123
|
+
// endpoints have no business in one.
|
|
124
|
+
if (config.scaffold?.scenarios) {
|
|
125
|
+
registerScenarioInstrumentation(config.scaffold.scenarios === 'auth');
|
|
126
|
+
}
|
|
117
127
|
const configModule = await loadUserModule(pikkuConfigFactory.file);
|
|
118
128
|
const servicesModule = await loadUserModule(singletonServicesFactory.file);
|
|
119
129
|
const userCreateConfig = configModule[pikkuConfigFactory.variable];
|
|
@@ -238,6 +248,7 @@ export const dev = pikkuSessionlessFunc({
|
|
|
238
248
|
if (consoleMount) {
|
|
239
249
|
logger.info(`Pikku Console available at http://${hostname}:${resolvedPort}${consoleMount.urlPrefix}`);
|
|
240
250
|
}
|
|
251
|
+
logger.info(serverReadyLine(hostname, resolvedPort));
|
|
241
252
|
let configWatcher;
|
|
242
253
|
let watcher;
|
|
243
254
|
process.once('SIGINT', async () => {
|
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
* `pikkuState`.
|
|
5
5
|
*/
|
|
6
6
|
export declare function loadUserBootstrap(pikkuDir: string): Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Load the generated `pikku-bootstrap-scenarios.gen.{ts,js}`, which imports the
|
|
9
|
+
* app bootstrap and then registers the scenarios, features and steps that are
|
|
10
|
+
* deliberately kept out of it. Falls back to the app bootstrap for a project
|
|
11
|
+
* generated before the split existed.
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadScenarioBootstrap(pikkuDir: string): Promise<void>;
|
|
7
14
|
/**
|
|
8
15
|
* Import a user-source TypeScript file (e.g. their config or services
|
|
9
16
|
* factory) so it can be loaded from inside @pikku/cli's compiled JS.
|
|
@@ -39,6 +39,27 @@ export async function loadUserBootstrap(pikkuDir) {
|
|
|
39
39
|
ensureTsxRegistered();
|
|
40
40
|
await importUserPath(bootstrapPath);
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Load the generated `pikku-bootstrap-scenarios.gen.{ts,js}`, which imports the
|
|
44
|
+
* app bootstrap and then registers the scenarios, features and steps that are
|
|
45
|
+
* deliberately kept out of it. Falls back to the app bootstrap for a project
|
|
46
|
+
* generated before the split existed.
|
|
47
|
+
*/
|
|
48
|
+
export async function loadScenarioBootstrap(pikkuDir) {
|
|
49
|
+
const scenarioTs = join(pikkuDir, 'pikku-bootstrap-scenarios.gen.ts');
|
|
50
|
+
const scenarioJs = join(pikkuDir, 'pikku-bootstrap-scenarios.gen.js');
|
|
51
|
+
const scenarioPath = existsSync(scenarioTs)
|
|
52
|
+
? scenarioTs
|
|
53
|
+
: existsSync(scenarioJs)
|
|
54
|
+
? scenarioJs
|
|
55
|
+
: null;
|
|
56
|
+
if (!scenarioPath) {
|
|
57
|
+
await loadUserBootstrap(pikkuDir);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
ensureTsxRegistered();
|
|
61
|
+
await importUserPath(scenarioPath);
|
|
62
|
+
}
|
|
42
63
|
/**
|
|
43
64
|
* Import a user-source TypeScript file (e.g. their config or services
|
|
44
65
|
* factory) so it can be loaded from inside @pikku/cli's compiled JS.
|
|
@@ -64,5 +64,18 @@ export const pikkuBootstrap = pikkuSessionlessFunc({
|
|
|
64
64
|
})
|
|
65
65
|
.join('\n');
|
|
66
66
|
await writeFileInDir(logger, config.bootstrapFile, allBootstrapImports);
|
|
67
|
+
// The scenario bootstrap is the only entry point that registers scenarios,
|
|
68
|
+
// features and their steps. It pulls in the app bootstrap first so a runner
|
|
69
|
+
// needs to load one file, and so the meta files it merges onto are already
|
|
70
|
+
// in place however this module is reached.
|
|
71
|
+
const scenarioBootstrapImports = [
|
|
72
|
+
config.bootstrapFile,
|
|
73
|
+
join(config.scenarioSchemaDirectory, 'register.gen.ts'),
|
|
74
|
+
config.scenarioStepsMetaFile,
|
|
75
|
+
config.scenarioStepsFile,
|
|
76
|
+
config.scenarioWiringsMetaFile,
|
|
77
|
+
config.scenarioWiringsFile,
|
|
78
|
+
].map((to) => `import '${getFileImportRelativePath(config.scenarioBootstrapFile, to, config.packageMappings, config.forceRelativeImports)}'`);
|
|
79
|
+
await writeFileInDir(logger, config.scenarioBootstrapFile, scenarioBootstrapImports.join('\n'));
|
|
67
80
|
},
|
|
68
81
|
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scenario runner's browser lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Two jobs, both kept out of the run loop so they are testable without
|
|
5
|
+
* launching a browser: deciding whether a run needs a driver at all (and
|
|
6
|
+
* failing early, with instructions, when it does and cannot have one), and
|
|
7
|
+
* making the driver's optional isolation and diagnostics safe to call
|
|
8
|
+
* unconditionally.
|
|
9
|
+
*/
|
|
10
|
+
import type { ScenarioBrowserFailure, ScenarioBrowserProvider } from '@pikku/core/workflow';
|
|
11
|
+
import type { ScenarioActorConfig } from '@pikku/core/services';
|
|
12
|
+
/** The default driver, used when a project names no other. */
|
|
13
|
+
export declare const DEFAULT_BROWSER_DRIVER = "@pikku/playwright";
|
|
14
|
+
export interface ScenarioBrowserDriverOptions {
|
|
15
|
+
secret: string;
|
|
16
|
+
actors: Record<string, ScenarioActorConfig>;
|
|
17
|
+
signInPath?: string;
|
|
18
|
+
failureDir?: string;
|
|
19
|
+
config: unknown;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* What a browser driver package exports.
|
|
23
|
+
*
|
|
24
|
+
* Nothing here is playwright-specific: a driver is anything that can hand a
|
|
25
|
+
* scenario step a `ScenarioBrowserProvider`. `@pikku/playwright` is the one
|
|
26
|
+
* pikku ships, named by `scenarios.browserDriver` only as a default.
|
|
27
|
+
*/
|
|
28
|
+
export interface ScenarioBrowserDriver {
|
|
29
|
+
createScenarioBrowserProvider?: (options: ScenarioBrowserDriverOptions) => ScenarioBrowserProvider;
|
|
30
|
+
/** The class form the bundled driver exports. */
|
|
31
|
+
PlaywrightScenarioBrowserProvider?: new (options: ScenarioBrowserDriverOptions) => ScenarioBrowserProvider;
|
|
32
|
+
/**
|
|
33
|
+
* Driver-specific config (headed, slowMo, timeouts) resolved from the env.
|
|
34
|
+
*
|
|
35
|
+
* A driver may resolve `appUrl` itself when the caller passes none — that is
|
|
36
|
+
* how a target known only at run time (a sandbox hostname in the env) is
|
|
37
|
+
* reached without a config file per deployment. A driver that answers with a
|
|
38
|
+
* placeholder rather than a real target must say so with
|
|
39
|
+
* `appUrlSource: 'default'`, which the runner reports as a missing appUrl.
|
|
40
|
+
*/
|
|
41
|
+
browserConfigFromEnv?: (overrides: {
|
|
42
|
+
appUrl?: string;
|
|
43
|
+
apiUrl: string;
|
|
44
|
+
}) => unknown;
|
|
45
|
+
}
|
|
46
|
+
export interface ResolveScenarioBrowserProviderOptions {
|
|
47
|
+
/** The environment name, for error messages that tell the user what to edit. */
|
|
48
|
+
environment: string;
|
|
49
|
+
apiUrl: string;
|
|
50
|
+
appUrl?: string;
|
|
51
|
+
secret: string;
|
|
52
|
+
actors: Record<string, ScenarioActorConfig>;
|
|
53
|
+
signInPath?: string;
|
|
54
|
+
/** Where failure screenshots are written. */
|
|
55
|
+
failureDir: string;
|
|
56
|
+
/** The scenarios that declared browser steps, named in the missing-driver error. */
|
|
57
|
+
browserScenarios: string[];
|
|
58
|
+
/** Package to drive the browser. Defaults to `@pikku/playwright`. */
|
|
59
|
+
driver?: string;
|
|
60
|
+
importDriver?: (specifier: string) => Promise<ScenarioBrowserDriver>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Both failure modes here are discovered before the first scenario starts. A
|
|
64
|
+
* missing `appUrl` or an uninstalled driver found mid-run costs a whole run to
|
|
65
|
+
* learn about.
|
|
66
|
+
*
|
|
67
|
+
* `appUrl` is checked after the driver has resolved its config rather than
|
|
68
|
+
* before, so a driver that knows the target from its own environment (a
|
|
69
|
+
* sandbox hostname) is allowed to supply it. The check is not dropped: a driver
|
|
70
|
+
* reporting `appUrlSource: 'default'` resolved nothing and fails the same way,
|
|
71
|
+
* because a run against a placeholder URL is worse than one that refuses to
|
|
72
|
+
* start.
|
|
73
|
+
*/
|
|
74
|
+
export declare const resolveScenarioBrowserProvider: ({ environment, apiUrl, appUrl, secret, actors, signInPath, failureDir, browserScenarios, driver, importDriver, }: ResolveScenarioBrowserProviderOptions) => Promise<ScenarioBrowserProvider>;
|
|
75
|
+
/**
|
|
76
|
+
* The three calls the run loop makes, safe against a run with no browser and
|
|
77
|
+
* against a driver that implements neither optional member.
|
|
78
|
+
*/
|
|
79
|
+
export interface ScenarioBrowserLifecycle {
|
|
80
|
+
reset(): Promise<void>;
|
|
81
|
+
captureFailure(label: string): Promise<ScenarioBrowserFailure[]>;
|
|
82
|
+
close(): Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
export declare const scenarioBrowserLifecycle: (provider: ScenarioBrowserProvider | undefined) => ScenarioBrowserLifecycle;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** The default driver, used when a project names no other. */
|
|
2
|
+
export const DEFAULT_BROWSER_DRIVER = '@pikku/playwright';
|
|
3
|
+
/**
|
|
4
|
+
* Both failure modes here are discovered before the first scenario starts. A
|
|
5
|
+
* missing `appUrl` or an uninstalled driver found mid-run costs a whole run to
|
|
6
|
+
* learn about.
|
|
7
|
+
*
|
|
8
|
+
* `appUrl` is checked after the driver has resolved its config rather than
|
|
9
|
+
* before, so a driver that knows the target from its own environment (a
|
|
10
|
+
* sandbox hostname) is allowed to supply it. The check is not dropped: a driver
|
|
11
|
+
* reporting `appUrlSource: 'default'` resolved nothing and fails the same way,
|
|
12
|
+
* because a run against a placeholder URL is worse than one that refuses to
|
|
13
|
+
* start.
|
|
14
|
+
*/
|
|
15
|
+
export const resolveScenarioBrowserProvider = async ({ environment, apiUrl, appUrl, secret, actors, signInPath, failureDir, browserScenarios, driver = DEFAULT_BROWSER_DRIVER, importDriver = (specifier) => import(specifier), }) => {
|
|
16
|
+
const module = await importDriver(driver).catch(() => {
|
|
17
|
+
const install = driver === DEFAULT_BROWSER_DRIVER
|
|
18
|
+
? `Run 'yarn add -D ${driver} @playwright/test'`
|
|
19
|
+
: `Install '${driver}', or point scenarios.browserDriver at a package that is`;
|
|
20
|
+
throw new Error(`Scenarios ${browserScenarios.join(', ')} declare browser steps but '${driver}' could not be loaded. ` +
|
|
21
|
+
`${install}, or run with --no-browser to skip them.`);
|
|
22
|
+
});
|
|
23
|
+
const config = (module.browserConfigFromEnv?.({ appUrl, apiUrl }) ?? {
|
|
24
|
+
appUrl,
|
|
25
|
+
apiUrl,
|
|
26
|
+
});
|
|
27
|
+
if (!config.appUrl || config.appUrlSource === 'default') {
|
|
28
|
+
throw new Error(`Scenario environment '${environment}' has browser steps but no 'appUrl', and '${driver}' resolved none from the environment. ` +
|
|
29
|
+
`Add it to scenarios.environments.${environment} in pikku.config.json, pass --app-url for a target that only exists at run time, or run with --no-browser to skip them.`);
|
|
30
|
+
}
|
|
31
|
+
const options = {
|
|
32
|
+
secret,
|
|
33
|
+
actors,
|
|
34
|
+
signInPath,
|
|
35
|
+
failureDir,
|
|
36
|
+
config,
|
|
37
|
+
};
|
|
38
|
+
if (module.createScenarioBrowserProvider) {
|
|
39
|
+
return module.createScenarioBrowserProvider(options);
|
|
40
|
+
}
|
|
41
|
+
if (module.PlaywrightScenarioBrowserProvider) {
|
|
42
|
+
return new module.PlaywrightScenarioBrowserProvider(options);
|
|
43
|
+
}
|
|
44
|
+
throw new Error(`'${driver}' is not a scenario browser driver: it exports neither ` +
|
|
45
|
+
`'createScenarioBrowserProvider' nor a provider class. A driver returns an object with ` +
|
|
46
|
+
`sessionFor() and close(), and optionally reset() and captureFailure().`);
|
|
47
|
+
};
|
|
48
|
+
export const scenarioBrowserLifecycle = (provider) => ({
|
|
49
|
+
// Deliberately NOT swallowed: a browser that cannot be reset would run the
|
|
50
|
+
// next scenario against the last one's session, and a scenario failing on
|
|
51
|
+
// its reset is far easier to diagnose than one failing on stale state.
|
|
52
|
+
reset: async () => {
|
|
53
|
+
await provider?.reset?.();
|
|
54
|
+
},
|
|
55
|
+
captureFailure: async (label) => {
|
|
56
|
+
try {
|
|
57
|
+
return (await provider?.captureFailure?.(label)) ?? [];
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// This runs while a scenario is already failing. Whatever went wrong
|
|
61
|
+
// here matters less than the error we were called to describe.
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
close: async () => {
|
|
66
|
+
await provider?.close();
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving `pikku scenario run <environment>` to the URLs the run targets.
|
|
3
|
+
*
|
|
4
|
+
* The configured environment is the base; `--api-url`/`--app-url` override it
|
|
5
|
+
* for one invocation, which is what makes a suite runnable against a target
|
|
6
|
+
* that only exists at run time (a freshly provisioned sandbox) without writing
|
|
7
|
+
* a pikku.config.json per deployment.
|
|
8
|
+
*/
|
|
9
|
+
/** One entry of `scenarios.environments` in pikku.config.json. */
|
|
10
|
+
export interface ScenarioEnvironment {
|
|
11
|
+
apiUrl: string;
|
|
12
|
+
signInPath?: string;
|
|
13
|
+
rpcPath?: string;
|
|
14
|
+
appUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ResolveScenarioEnvironmentOptions {
|
|
17
|
+
/** The name given on the command line. Must exist in config. */
|
|
18
|
+
environment: string;
|
|
19
|
+
environments: Record<string, ScenarioEnvironment>;
|
|
20
|
+
/** `--api-url`, overriding the configured `apiUrl`. */
|
|
21
|
+
apiUrl?: string;
|
|
22
|
+
/** `--app-url`, overriding the configured `appUrl`. */
|
|
23
|
+
appUrl?: string;
|
|
24
|
+
/** `--spawn`, which is only coherent against a local `apiUrl`. */
|
|
25
|
+
spawn?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Applied once, so actors, step env, the browser driver and the spawned server
|
|
29
|
+
* all see the same target. The environment is still looked up by name: the
|
|
30
|
+
* flags override fields on a configured environment, they do not invent one.
|
|
31
|
+
*/
|
|
32
|
+
export declare const resolveScenarioEnvironment: ({ environment, environments, apiUrl, appUrl, spawn, }: ResolveScenarioEnvironmentOptions) => ScenarioEnvironment;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving `pikku scenario run <environment>` to the URLs the run targets.
|
|
3
|
+
*
|
|
4
|
+
* The configured environment is the base; `--api-url`/`--app-url` override it
|
|
5
|
+
* for one invocation, which is what makes a suite runnable against a target
|
|
6
|
+
* that only exists at run time (a freshly provisioned sandbox) without writing
|
|
7
|
+
* a pikku.config.json per deployment.
|
|
8
|
+
*/
|
|
9
|
+
const LOCAL_HOSTNAMES = new Set([
|
|
10
|
+
'localhost',
|
|
11
|
+
'127.0.0.1',
|
|
12
|
+
'0.0.0.0',
|
|
13
|
+
'::1',
|
|
14
|
+
'[::1]',
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* Absolute URLs only: the value is concatenated with paths (`${apiUrl}/rpc`)
|
|
18
|
+
* and parsed by the spawn path, so a relative one fails far from its cause.
|
|
19
|
+
*/
|
|
20
|
+
const assertAbsoluteUrl = (flag, value) => {
|
|
21
|
+
let url;
|
|
22
|
+
try {
|
|
23
|
+
url = new URL(value);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new Error(`${flag} '${value}' is not a valid absolute URL — pass one including the scheme, e.g. ${flag} https://sandbox-a1b2.example.com/api`);
|
|
27
|
+
}
|
|
28
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
29
|
+
throw new Error(`${flag} '${value}' must be an http(s) URL, not '${url.protocol.replace(':', '')}'.`);
|
|
30
|
+
}
|
|
31
|
+
return url;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Applied once, so actors, step env, the browser driver and the spawned server
|
|
35
|
+
* all see the same target. The environment is still looked up by name: the
|
|
36
|
+
* flags override fields on a configured environment, they do not invent one.
|
|
37
|
+
*/
|
|
38
|
+
export const resolveScenarioEnvironment = ({ environment, environments, apiUrl, appUrl, spawn = false, }) => {
|
|
39
|
+
const configured = environments[environment];
|
|
40
|
+
if (!configured) {
|
|
41
|
+
const known = Object.keys(environments);
|
|
42
|
+
throw new Error(`Unknown scenario environment '${environment}'. ` +
|
|
43
|
+
(known.length
|
|
44
|
+
? `Configured environments: ${known.join(', ')}`
|
|
45
|
+
: `Add scenarios.environments to pikku.config.json, e.g. { "${environment}": { "apiUrl": "https://app.example.com/api" } }`));
|
|
46
|
+
}
|
|
47
|
+
const resolved = { ...configured };
|
|
48
|
+
if (apiUrl !== undefined) {
|
|
49
|
+
const url = assertAbsoluteUrl('--api-url', apiUrl);
|
|
50
|
+
if (spawn && !LOCAL_HOSTNAMES.has(url.hostname)) {
|
|
51
|
+
throw new Error(`--spawn starts a server on this machine, but --api-url points at '${url.hostname}'. ` +
|
|
52
|
+
`Drop --spawn to run against an already-serving target, or point --api-url at localhost.`);
|
|
53
|
+
}
|
|
54
|
+
resolved.apiUrl = apiUrl;
|
|
55
|
+
}
|
|
56
|
+
if (appUrl !== undefined) {
|
|
57
|
+
assertAbsoluteUrl('--app-url', appUrl);
|
|
58
|
+
resolved.appUrl = appUrl;
|
|
59
|
+
}
|
|
60
|
+
return resolved;
|
|
61
|
+
};
|