@siteoshq/cli 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/cli.js +127 -25
- package/dist/cli.js.map +1 -1
- package/package.json +18 -20
package/README.md
CHANGED
|
@@ -267,3 +267,17 @@ answers from the working database, retaining only a reserved-key tombstone. That
|
|
|
267
267
|
reused. Shared Environment credentials remain active for other forms. Sync refuses archived/deleted
|
|
268
268
|
keys and never removes forms missing from a local manifest. Backups follow deployment retention;
|
|
269
269
|
they are not an in-app restore mechanism.
|
|
270
|
+
|
|
271
|
+
### Automatic Forms deployment
|
|
272
|
+
|
|
273
|
+
Generate Forms JSON from the shared host validation schema during build. Install a release-only
|
|
274
|
+
key once with `siteos forms deployment-key issue --environment production --install --json`.
|
|
275
|
+
Store `SITEOS_FORMS_DEPLOYMENT_KEY` in the CI secret store and configure explicit
|
|
276
|
+
`SITEOS_FORMS_PUBLIC_URL`. Keep the submission credential on the website server.
|
|
277
|
+
|
|
278
|
+
Run `siteos forms deploy --manifest .siteos/forms/manifest.json --json` before routing traffic to a
|
|
279
|
+
new release. This needs no interactive Auth or local Project state and publishes atomically.
|
|
280
|
+
Definitions include a generated SHA-256 `sourceExportId`; runtime sends it as `contractVersion`.
|
|
281
|
+
A changed field requires a normal build/release, not a second hand-edited validation schema.
|
|
282
|
+
`forms deployment-key list|revoke` manages release keys; submission keys cannot publish.
|
|
283
|
+
See the SiteOS Forms skill for the portable generator and version serialization contract.
|
package/dist/cli.js
CHANGED
|
@@ -3133,11 +3133,33 @@ function resolveSiteOSFormsBaseUrl(env = process.env) {
|
|
|
3133
3133
|
env[SITEOS_FORMS_BASE_URL_ENV]?.trim() || DEFAULT_SITEOS_FORMS_BASE_URL
|
|
3134
3134
|
);
|
|
3135
3135
|
}
|
|
3136
|
+
var deploymentResponseSchema = z9.object({
|
|
3137
|
+
environmentId: opaqueIdentifierSchema,
|
|
3138
|
+
forms: z9.array(
|
|
3139
|
+
z9.object({
|
|
3140
|
+
formKey: opaqueIdentifierSchema,
|
|
3141
|
+
formId: opaqueIdentifierSchema,
|
|
3142
|
+
contractVersion: z9.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
3143
|
+
versionId: opaqueIdentifierSchema,
|
|
3144
|
+
version: z9.number().int().positive()
|
|
3145
|
+
})
|
|
3146
|
+
)
|
|
3147
|
+
});
|
|
3136
3148
|
function createSiteOSFormsApiClient(options) {
|
|
3137
3149
|
const apiBaseUrl = normalizeFormsOrigin(options.apiBaseUrl);
|
|
3138
3150
|
const inboxPath = (input) => `/api/v1/forms/environments/${encodeURIComponent(input.environmentId)}/forms/${encodeURIComponent(input.formId)}/submissions`;
|
|
3139
3151
|
const definitionPath = (input) => `/api/v1/forms/environments/${encodeURIComponent(input.environmentId)}/forms/${encodeURIComponent(input.formId)}`;
|
|
3152
|
+
const keyCollection = (input) => options.deploymentKeys ? `${environmentItemPath(input)}/deployment-keys` : credentialCollectionPath(input);
|
|
3140
3153
|
return {
|
|
3154
|
+
publishDefinitions: (input) => requestJson2({
|
|
3155
|
+
apiBaseUrl,
|
|
3156
|
+
fetchImpl: options.fetchImpl,
|
|
3157
|
+
method: "POST",
|
|
3158
|
+
path: "/api/forms/deployments",
|
|
3159
|
+
body: { definitions: input.definitions },
|
|
3160
|
+
headers: { "x-siteos-forms-deployment-key": input.credential },
|
|
3161
|
+
responseSchema: deploymentResponseSchema
|
|
3162
|
+
}),
|
|
3141
3163
|
listDefinitions: (input) => requestJson2({
|
|
3142
3164
|
apiBaseUrl,
|
|
3143
3165
|
fetchImpl: options.fetchImpl,
|
|
@@ -3212,8 +3234,10 @@ function createSiteOSFormsApiClient(options) {
|
|
|
3212
3234
|
fetchImpl: options.fetchImpl,
|
|
3213
3235
|
headers: managementHeaders(input),
|
|
3214
3236
|
method: "POST",
|
|
3215
|
-
path:
|
|
3216
|
-
responseSchema: formsCredentialExchangeSchema
|
|
3237
|
+
path: keyCollection(input),
|
|
3238
|
+
responseSchema: options.deploymentKeys ? formsCredentialExchangeSchema.extend({
|
|
3239
|
+
token: z9.string().regex(/^pfd_[A-Za-z0-9_-]{43}$/)
|
|
3240
|
+
}) : formsCredentialExchangeSchema
|
|
3217
3241
|
}),
|
|
3218
3242
|
listEnvironments: (input) => requestJson2({
|
|
3219
3243
|
apiBaseUrl,
|
|
@@ -3236,7 +3260,7 @@ function createSiteOSFormsApiClient(options) {
|
|
|
3236
3260
|
fetchImpl: options.fetchImpl,
|
|
3237
3261
|
headers: managementHeaders(input),
|
|
3238
3262
|
method: "GET",
|
|
3239
|
-
path:
|
|
3263
|
+
path: keyCollection(input),
|
|
3240
3264
|
responseSchema: formsCredentialListSchema
|
|
3241
3265
|
}),
|
|
3242
3266
|
revokeCredential: (input) => requestJson2({
|
|
@@ -3244,7 +3268,7 @@ function createSiteOSFormsApiClient(options) {
|
|
|
3244
3268
|
fetchImpl: options.fetchImpl,
|
|
3245
3269
|
headers: managementHeaders(input),
|
|
3246
3270
|
method: "DELETE",
|
|
3247
|
-
path: credentialItemPath(input),
|
|
3271
|
+
path: options.deploymentKeys ? `${keyCollection(input)}/${encodeURIComponent(input.credentialId)}` : credentialItemPath(input),
|
|
3248
3272
|
responseSchema: formsCredentialMetadataSchema
|
|
3249
3273
|
}),
|
|
3250
3274
|
rotateCredential: (input) => requestJson2({
|
|
@@ -3254,7 +3278,9 @@ function createSiteOSFormsApiClient(options) {
|
|
|
3254
3278
|
headers: managementHeaders(input),
|
|
3255
3279
|
method: "POST",
|
|
3256
3280
|
path: credentialRotatePath(input),
|
|
3257
|
-
responseSchema: formsCredentialExchangeSchema
|
|
3281
|
+
responseSchema: options.deploymentKeys ? formsCredentialExchangeSchema.extend({
|
|
3282
|
+
token: z9.string().regex(/^pfd_[A-Za-z0-9_-]{43}$/)
|
|
3283
|
+
}) : formsCredentialExchangeSchema
|
|
3258
3284
|
}),
|
|
3259
3285
|
submitForm: (input) => requestJson2({
|
|
3260
3286
|
apiBaseUrl,
|
|
@@ -3288,7 +3314,9 @@ async function requestJson2(options) {
|
|
|
3288
3314
|
...options.body === void 0 ? {} : { "Content-Type": "application/json" },
|
|
3289
3315
|
...options.headers
|
|
3290
3316
|
},
|
|
3291
|
-
method: options.method
|
|
3317
|
+
method: options.method,
|
|
3318
|
+
redirect: "error",
|
|
3319
|
+
signal: AbortSignal.timeout(3e4)
|
|
3292
3320
|
});
|
|
3293
3321
|
} catch {
|
|
3294
3322
|
throw new SiteOSFormsApiError({
|
|
@@ -3381,7 +3409,7 @@ import {
|
|
|
3381
3409
|
} from "fs/promises";
|
|
3382
3410
|
import path11 from "path";
|
|
3383
3411
|
var SITEOS_FORMS_SUBMISSION_CREDENTIAL_ENV = "SITEOS_FORMS_SUBMISSION_CREDENTIAL";
|
|
3384
|
-
var
|
|
3412
|
+
var SITEOS_FORMS_DEPLOYMENT_KEY_ENV = "SITEOS_FORMS_DEPLOYMENT_KEY";
|
|
3385
3413
|
var SiteOSFormsCredentialStorageError = class extends Error {
|
|
3386
3414
|
code;
|
|
3387
3415
|
constructor(code) {
|
|
@@ -3393,6 +3421,8 @@ var SiteOSFormsCredentialStorageError = class extends Error {
|
|
|
3393
3421
|
}
|
|
3394
3422
|
};
|
|
3395
3423
|
async function prepareSiteOSFormsCredentialInstallation(options) {
|
|
3424
|
+
const variable = options.deployment ? SITEOS_FORMS_DEPLOYMENT_KEY_ENV : SITEOS_FORMS_SUBMISSION_CREDENTIAL_ENV;
|
|
3425
|
+
const credentialPattern = options.deployment ? /^pfd_[A-Za-z0-9_-]{43}$/ : /^pfs_[A-Za-z0-9_-]{22}$/;
|
|
3396
3426
|
const projectRoot = path11.resolve(options.projectRoot);
|
|
3397
3427
|
const dotenvPath = path11.join(projectRoot, ".env");
|
|
3398
3428
|
try {
|
|
@@ -3419,7 +3449,7 @@ async function prepareSiteOSFormsCredentialInstallation(options) {
|
|
|
3419
3449
|
}
|
|
3420
3450
|
const currentContent = await readSafeDotenv(dotenvPath);
|
|
3421
3451
|
await writeCredentialAtomically({
|
|
3422
|
-
content: setCredential(currentContent, credential),
|
|
3452
|
+
content: setCredential(currentContent, credential, variable),
|
|
3423
3453
|
dotenvPath,
|
|
3424
3454
|
projectRoot
|
|
3425
3455
|
});
|
|
@@ -3457,7 +3487,10 @@ async function readSafeDotenv(dotenvPath) {
|
|
|
3457
3487
|
);
|
|
3458
3488
|
}
|
|
3459
3489
|
const content = dotenvStats ? await readFile4(dotenvPath, "utf8") : "";
|
|
3460
|
-
if (
|
|
3490
|
+
if ([
|
|
3491
|
+
SITEOS_FORMS_SUBMISSION_CREDENTIAL_ENV,
|
|
3492
|
+
SITEOS_FORMS_DEPLOYMENT_KEY_ENV
|
|
3493
|
+
].some((variable) => countCredentialEntries(content, variable) > 1)) {
|
|
3461
3494
|
throw new SiteOSFormsCredentialStorageError(
|
|
3462
3495
|
"FORMS_CREDENTIAL_STORAGE_INVALID"
|
|
3463
3496
|
);
|
|
@@ -3485,21 +3518,16 @@ async function writeCredentialAtomically(options) {
|
|
|
3485
3518
|
await rm3(temporaryPath, { force: true }).catch(() => void 0);
|
|
3486
3519
|
}
|
|
3487
3520
|
}
|
|
3488
|
-
function setCredential(content, credential) {
|
|
3489
|
-
const assignment = `${
|
|
3490
|
-
const existing = new RegExp(
|
|
3491
|
-
`^${SITEOS_FORMS_SUBMISSION_CREDENTIAL_ENV}=[^\\r\\n]*(?=\\r?$)`,
|
|
3492
|
-
"m"
|
|
3493
|
-
);
|
|
3521
|
+
function setCredential(content, credential, variable) {
|
|
3522
|
+
const assignment = `${variable}=${credential}`;
|
|
3523
|
+
const existing = new RegExp(`^${variable}=[^\\r\\n]*(?=\\r?$)`, "m");
|
|
3494
3524
|
if (existing.test(content)) return content.replace(existing, assignment);
|
|
3495
3525
|
const newline = content.includes("\r\n") ? "\r\n" : "\n";
|
|
3496
3526
|
if (content.length === 0) return `${assignment}${newline}`;
|
|
3497
3527
|
return `${content}${content.endsWith("\n") ? "" : newline}${assignment}${newline}`;
|
|
3498
3528
|
}
|
|
3499
|
-
function countCredentialEntries(content) {
|
|
3500
|
-
return content.split(/\r?\n/u).filter(
|
|
3501
|
-
(line) => line.startsWith(`${SITEOS_FORMS_SUBMISSION_CREDENTIAL_ENV}=`)
|
|
3502
|
-
).length;
|
|
3529
|
+
function countCredentialEntries(content, variable) {
|
|
3530
|
+
return content.split(/\r?\n/u).filter((line) => line.startsWith(`${variable}=`)).length;
|
|
3503
3531
|
}
|
|
3504
3532
|
function isDotenvIgnored(projectRoot) {
|
|
3505
3533
|
return new Promise((resolve, reject) => {
|
|
@@ -3670,6 +3698,10 @@ var FORMS_HELP = `Usage:
|
|
|
3670
3698
|
siteos forms submissions list --environment <slug> --form <form-id> [--query <text>] [--status <status>] [--from <ISO>] [--to <ISO>] [--limit <1-100>] [--cursor <cursor>] [--json]
|
|
3671
3699
|
siteos forms submissions read --environment <slug> --form <form-id> --submission <id> [--json]
|
|
3672
3700
|
siteos forms submissions status --environment <slug> --form <form-id> --submission <id> --status <new|read|archived|spam> --expected-status <status> [--json]
|
|
3701
|
+
siteos forms deploy --manifest <path> [--json]
|
|
3702
|
+
siteos forms deployment-key issue --environment <slug> --install [--name <name>] [--json]
|
|
3703
|
+
siteos forms deployment-key list --environment <slug> [--json]
|
|
3704
|
+
siteos forms deployment-key revoke --environment <slug> --credential <id> [--json]
|
|
3673
3705
|
siteos forms submit --input <path> [--json]
|
|
3674
3706
|
|
|
3675
3707
|
Manage SiteOS Forms Environments, definitions, credentials, and submission smoke tests.`;
|
|
@@ -3681,6 +3713,8 @@ async function runFormsCommand(options) {
|
|
|
3681
3713
|
stdout: FORMS_HELP
|
|
3682
3714
|
};
|
|
3683
3715
|
}
|
|
3716
|
+
if (args2[0] === "deploy")
|
|
3717
|
+
return runDeployment({ ...options, args: args2.slice(1) });
|
|
3684
3718
|
if (args2[0] !== "submit" && args2[0] !== "project") {
|
|
3685
3719
|
try {
|
|
3686
3720
|
args2 = await commonEnvironmentArguments(options, "forms", args2);
|
|
@@ -3717,6 +3751,13 @@ async function runFormsCommand(options) {
|
|
|
3717
3751
|
args: args2.slice(1)
|
|
3718
3752
|
});
|
|
3719
3753
|
}
|
|
3754
|
+
if (args2[0] === "deployment-key") {
|
|
3755
|
+
return runCredentialCommand({
|
|
3756
|
+
...options,
|
|
3757
|
+
args: args2.slice(1),
|
|
3758
|
+
deployment: true
|
|
3759
|
+
});
|
|
3760
|
+
}
|
|
3720
3761
|
if (args2[0] === "credential") {
|
|
3721
3762
|
return runCredentialCommand({
|
|
3722
3763
|
...options,
|
|
@@ -4052,6 +4093,11 @@ async function runCredentialCommand(options) {
|
|
|
4052
4093
|
if (!action || !["issue", "list", "revoke", "rotate"].includes(action)) {
|
|
4053
4094
|
return usageError2("Unknown SiteOS forms credential command.");
|
|
4054
4095
|
}
|
|
4096
|
+
if (options.deployment && action === "rotate")
|
|
4097
|
+
return usageError2(
|
|
4098
|
+
"Issue a new deployment key, update the release secret, then revoke the old key."
|
|
4099
|
+
);
|
|
4100
|
+
const variable = options.deployment ? SITEOS_FORMS_DEPLOYMENT_KEY_ENV : SITEOS_FORMS_SUBMISSION_CREDENTIAL_ENV;
|
|
4055
4101
|
const parsed = parseCredentialFlags(action, options.args.slice(1));
|
|
4056
4102
|
if (!parsed.ok) return parsed.error;
|
|
4057
4103
|
const context = await loadCredentialManagementContext({
|
|
@@ -4123,7 +4169,7 @@ Environment: ${parsed.environmentSlug}`
|
|
|
4123
4169
|
credential,
|
|
4124
4170
|
environment: parsed.environmentSlug,
|
|
4125
4171
|
installed: {
|
|
4126
|
-
variable
|
|
4172
|
+
variable
|
|
4127
4173
|
},
|
|
4128
4174
|
operation: action === "issue" ? "issued" : "rotated"
|
|
4129
4175
|
};
|
|
@@ -4133,7 +4179,7 @@ Environment: ${parsed.environmentSlug}`
|
|
|
4133
4179
|
`SiteOS Forms credential ${output.operation} and installed.`,
|
|
4134
4180
|
`Credential: ${credential.id}`,
|
|
4135
4181
|
`Environment: ${parsed.environmentSlug}`,
|
|
4136
|
-
`Variable: ${
|
|
4182
|
+
`Variable: ${variable}`
|
|
4137
4183
|
].join("\n")
|
|
4138
4184
|
};
|
|
4139
4185
|
} catch (error) {
|
|
@@ -4144,14 +4190,21 @@ async function loadCredentialManagementContext(options) {
|
|
|
4144
4190
|
try {
|
|
4145
4191
|
const local = await readLocalCommonProject(options);
|
|
4146
4192
|
const rootDir = local?.rootDir ?? (await requireFormsProjectReference(options.cwd ?? process.cwd())).rootDir;
|
|
4147
|
-
const installation = options.install ? await (options.prepareCredentialInstallation ?? prepareSiteOSFormsCredentialInstallation)({
|
|
4193
|
+
const installation = options.install ? await (options.prepareCredentialInstallation ?? prepareSiteOSFormsCredentialInstallation)({
|
|
4194
|
+
projectRoot: rootDir,
|
|
4195
|
+
...options.deployment ? { deployment: true } : {}
|
|
4196
|
+
}) : void 0;
|
|
4148
4197
|
const context = await loadFormsManagementContext(
|
|
4149
4198
|
options,
|
|
4150
4199
|
"forms:credential:manage"
|
|
4151
4200
|
);
|
|
4152
4201
|
if (!context.ok) return context;
|
|
4153
4202
|
return {
|
|
4154
|
-
client:
|
|
4203
|
+
client: options.deployment ? createSiteOSFormsApiClient({
|
|
4204
|
+
apiBaseUrl: context.apiBaseUrl,
|
|
4205
|
+
fetchImpl: options.fetchImpl ?? fetch,
|
|
4206
|
+
deploymentKeys: true
|
|
4207
|
+
}) : context.client,
|
|
4155
4208
|
grant: context.grant,
|
|
4156
4209
|
...installation ? { installation } : {},
|
|
4157
4210
|
ok: true,
|
|
@@ -5255,6 +5308,55 @@ function createFormsUiOutput(input) {
|
|
|
5255
5308
|
url
|
|
5256
5309
|
};
|
|
5257
5310
|
}
|
|
5311
|
+
async function runDeployment(options) {
|
|
5312
|
+
const parsed = parseDefinitionSourceFlags(options.args, {
|
|
5313
|
+
environmentRequired: false,
|
|
5314
|
+
usage: "Usage: siteos forms deploy --manifest <path> [--json]"
|
|
5315
|
+
});
|
|
5316
|
+
if (!parsed.ok) return parsed.error;
|
|
5317
|
+
const loaded = await loadFormDefinitions({
|
|
5318
|
+
cwd: options.cwd,
|
|
5319
|
+
source: parsed.source
|
|
5320
|
+
});
|
|
5321
|
+
if (!loaded.ok) return loaded.error;
|
|
5322
|
+
const env = options.env ?? process.env;
|
|
5323
|
+
const credential = env[SITEOS_FORMS_DEPLOYMENT_KEY_ENV]?.trim();
|
|
5324
|
+
const apiBaseUrl = env.SITEOS_FORMS_PUBLIC_URL?.trim();
|
|
5325
|
+
if (!credential || !/^pfd_[A-Za-z0-9_-]{43}$/.test(credential) || !apiBaseUrl)
|
|
5326
|
+
return {
|
|
5327
|
+
exitCode: 1,
|
|
5328
|
+
stderr: "Configure SITEOS_FORMS_PUBLIC_URL and SITEOS_FORMS_DEPLOYMENT_KEY in the release environment."
|
|
5329
|
+
};
|
|
5330
|
+
try {
|
|
5331
|
+
const client = createSiteOSFormsApiClient({
|
|
5332
|
+
apiBaseUrl,
|
|
5333
|
+
fetchImpl: options.fetchImpl ?? fetch
|
|
5334
|
+
});
|
|
5335
|
+
const result = await client.publishDefinitions({
|
|
5336
|
+
credential,
|
|
5337
|
+
definitions: loaded.definitions.map((item) => item.value)
|
|
5338
|
+
});
|
|
5339
|
+
const expected = new Map(
|
|
5340
|
+
loaded.definitions.map((item) => [
|
|
5341
|
+
item.value.formKey,
|
|
5342
|
+
item.value.sourceExportId
|
|
5343
|
+
])
|
|
5344
|
+
);
|
|
5345
|
+
if (result.forms.length !== expected.size || new Set(result.forms.map((item) => item.formKey)).size !== expected.size || result.forms.some(
|
|
5346
|
+
(item) => !expected.has(item.formKey) || expected.get(item.formKey) !== item.contractVersion
|
|
5347
|
+
))
|
|
5348
|
+
return {
|
|
5349
|
+
exitCode: 1,
|
|
5350
|
+
stderr: "The published Forms versions do not match this build."
|
|
5351
|
+
};
|
|
5352
|
+
return {
|
|
5353
|
+
exitCode: 0,
|
|
5354
|
+
stdout: parsed.json ? stringifySafeJson(result) : `Published ${result.forms.length} Forms contracts for Environment ${result.environmentId}.`
|
|
5355
|
+
};
|
|
5356
|
+
} catch (error) {
|
|
5357
|
+
return formatApiError(error);
|
|
5358
|
+
}
|
|
5359
|
+
}
|
|
5258
5360
|
|
|
5259
5361
|
// src/package-metadata.ts
|
|
5260
5362
|
import { readFileSync } from "fs";
|
|
@@ -5299,7 +5401,7 @@ var projectSchema = z11.object({
|
|
|
5299
5401
|
}).strict();
|
|
5300
5402
|
var projectListSchema = z11.object({ projects: z11.array(projectSchema) }).strict();
|
|
5301
5403
|
var projectResponseSchema = z11.object({ project: projectSchema }).strict();
|
|
5302
|
-
var
|
|
5404
|
+
var deploymentResponseSchema2 = z11.object({
|
|
5303
5405
|
checks: z11.array(
|
|
5304
5406
|
z11.object({
|
|
5305
5407
|
checkId: z11.string().min(1),
|
|
@@ -5404,7 +5506,7 @@ function createPulseApiClient(input) {
|
|
|
5404
5506
|
grant: options.grant,
|
|
5405
5507
|
method: "POST",
|
|
5406
5508
|
path: "/api/cli/v1/deployments",
|
|
5407
|
-
schema:
|
|
5509
|
+
schema: deploymentResponseSchema2
|
|
5408
5510
|
});
|
|
5409
5511
|
}
|
|
5410
5512
|
};
|