@siteoshq/cli 1.3.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/dist/cli.js CHANGED
@@ -2936,6 +2936,8 @@ function isCliExitCode(value) {
2936
2936
  import { readFile as readFile5 } from "fs/promises";
2937
2937
  import path12 from "path";
2938
2938
  import { z as z10 } from "zod";
2939
+ import { Ajv } from "ajv";
2940
+ import addFormats from "ajv-formats";
2939
2941
 
2940
2942
  // src/siteos-forms-api.ts
2941
2943
  import { z as z9 } from "zod";
@@ -2971,12 +2973,82 @@ var formDefinitionSyncResponseSchema = z9.object({
2971
2973
  success: z9.literal(true),
2972
2974
  version: z9.number().int().positive()
2973
2975
  }).strict();
2976
+ var definitionSchema = z9.object({
2977
+ id: opaqueIdentifierSchema,
2978
+ environmentId: opaqueIdentifierSchema,
2979
+ formKey: opaqueIdentifierSchema,
2980
+ name: z9.string(),
2981
+ sourcePagePath: z9.string().nullable(),
2982
+ activeVersion: z9.number().int().positive(),
2983
+ activeVersionId: opaqueIdentifierSchema,
2984
+ status: z9.enum(["active", "inactive"]),
2985
+ submissionCount: z9.number().int().nonnegative(),
2986
+ lastSubmittedAt: z9.string().datetime({ offset: true }).nullable(),
2987
+ createdAt: z9.string().datetime({ offset: true }),
2988
+ updatedAt: z9.string().datetime({ offset: true })
2989
+ });
2990
+ var definitionListSchema = z9.object({
2991
+ definitions: z9.array(definitionSchema)
2992
+ });
2993
+ var definitionContextSchema = z9.object({
2994
+ definition: definitionSchema,
2995
+ revision: z9.number().int().positive(),
2996
+ canManage: z9.boolean(),
2997
+ project: z9.object({ id: opaqueIdentifierSchema, name: z9.string() }),
2998
+ environment: z9.object({
2999
+ id: opaqueIdentifierSchema,
3000
+ name: z9.string(),
3001
+ slug: z9.string()
3002
+ })
3003
+ });
3004
+ var definitionChangeResponseSchema = z9.object({
3005
+ deleted: z9.boolean(),
3006
+ formId: opaqueIdentifierSchema
3007
+ });
2974
3008
  var formSubmissionResponseSchema = z9.object({
2975
3009
  formId: opaqueIdentifierSchema,
2976
3010
  receivedAt: z9.string().datetime({ offset: true }),
2977
3011
  submissionId: opaqueIdentifierSchema,
2978
3012
  success: z9.literal(true)
2979
3013
  }).strict();
3014
+ var formsSubmissionStatusSchema = z9.enum([
3015
+ "new",
3016
+ "read",
3017
+ "archived",
3018
+ "spam"
3019
+ ]);
3020
+ var submissionSummarySchema = z9.object({
3021
+ id: opaqueIdentifierSchema,
3022
+ formId: opaqueIdentifierSchema,
3023
+ normalized: z9.record(
3024
+ z9.union([z9.string(), z9.number(), z9.boolean(), z9.null()])
3025
+ ),
3026
+ status: formsSubmissionStatusSchema,
3027
+ submittedAt: z9.string().datetime({ offset: true })
3028
+ });
3029
+ var submissionDetailSchema = submissionSummarySchema.extend({
3030
+ payload: z9.record(z9.unknown()),
3031
+ fields: z9.array(
3032
+ z9.object({
3033
+ key: z9.string(),
3034
+ label: z9.string().optional(),
3035
+ kind: z9.string().optional(),
3036
+ displayRole: z9.enum(["primary", "secondary"]).optional(),
3037
+ multiple: z9.boolean().optional(),
3038
+ options: z9.array(z9.object({ value: z9.string(), label: z9.string() })).optional()
3039
+ })
3040
+ ),
3041
+ version: z9.number().int().positive(),
3042
+ versionId: opaqueIdentifierSchema
3043
+ });
3044
+ var submissionPageSchema = z9.object({
3045
+ submissions: z9.array(submissionSummarySchema),
3046
+ total: z9.number().int().nonnegative(),
3047
+ nextCursor: z9.string().nullable()
3048
+ });
3049
+ var submissionDetailResponseSchema = z9.object({
3050
+ submission: submissionDetailSchema
3051
+ });
2980
3052
  var formsCredentialMetadataSchema = z9.object({
2981
3053
  createdAt: z9.string().datetime({ offset: true }),
2982
3054
  id: z9.string().trim().min(1).max(255),
@@ -2993,6 +3065,9 @@ var formsCredentialExchangeSchema = formsCredentialMetadataSchema.extend({
2993
3065
  token: z9.string().regex(/^pfs_[A-Za-z0-9_-]{22}$/)
2994
3066
  });
2995
3067
  var formsErrorCodeSchema = z9.enum([
3068
+ "AUTHENTICATION_REQUIRED",
3069
+ "FORBIDDEN",
3070
+ "CONFLICT",
2996
3071
  "INVALID_REQUEST",
2997
3072
  "INVALID_INPUT",
2998
3073
  "VALIDATION_FAILED",
@@ -3000,6 +3075,8 @@ var formsErrorCodeSchema = z9.enum([
3000
3075
  "UNAUTHORIZED",
3001
3076
  "IDENTITY_REQUIRED",
3002
3077
  "NOT_ALLOWED",
3078
+ "FORM_ARCHIVED",
3079
+ "FORM_DELETED",
3003
3080
  "NOT_FOUND",
3004
3081
  "PROJECT_NOT_FOUND",
3005
3082
  "PROJECT_CONFLICT",
@@ -3012,6 +3089,9 @@ var formsErrorCodeSchema = z9.enum([
3012
3089
  "INTERNAL_ERROR"
3013
3090
  ]);
3014
3091
  var safeFormsErrorMessages = {
3092
+ AUTHENTICATION_REQUIRED: "Sign in to SiteOS before reading Forms submissions.",
3093
+ FORBIDDEN: "Forms management access is denied.",
3094
+ CONFLICT: "The resource changed or this idempotency key belongs to different data. Read the current state before retrying.",
3015
3095
  INVALID_REQUEST: "The Forms request is invalid.",
3016
3096
  INVALID_INPUT: "Invalid input data.",
3017
3097
  VALIDATION_FAILED: "Validation failed.",
@@ -3019,6 +3099,8 @@ var safeFormsErrorMessages = {
3019
3099
  UNAUTHORIZED: "Forms management authorization is invalid.",
3020
3100
  IDENTITY_REQUIRED: "Forms browser identity is required.",
3021
3101
  NOT_ALLOWED: "Forms management access is denied.",
3102
+ FORM_ARCHIVED: "This form is archived. Restore it explicitly before syncing or sending new submissions.",
3103
+ FORM_DELETED: "This form was permanently deleted. Use a new form key for a new form.",
3022
3104
  NOT_FOUND: "The requested resource was not found.",
3023
3105
  PROJECT_NOT_FOUND: "The Forms Project was not found.",
3024
3106
  PROJECT_CONFLICT: "The Forms Project already exists.",
@@ -3051,9 +3133,83 @@ function resolveSiteOSFormsBaseUrl(env = process.env) {
3051
3133
  env[SITEOS_FORMS_BASE_URL_ENV]?.trim() || DEFAULT_SITEOS_FORMS_BASE_URL
3052
3134
  );
3053
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
+ });
3054
3148
  function createSiteOSFormsApiClient(options) {
3055
3149
  const apiBaseUrl = normalizeFormsOrigin(options.apiBaseUrl);
3150
+ const inboxPath = (input) => `/api/v1/forms/environments/${encodeURIComponent(input.environmentId)}/forms/${encodeURIComponent(input.formId)}/submissions`;
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);
3056
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
+ }),
3163
+ listDefinitions: (input) => requestJson2({
3164
+ apiBaseUrl,
3165
+ fetchImpl: options.fetchImpl,
3166
+ headers: managementHeaders(input),
3167
+ method: "GET",
3168
+ path: `/api/v1/forms/environments/${encodeURIComponent(input.environmentId)}/definitions?status=${input.status}`,
3169
+ responseSchema: definitionListSchema
3170
+ }),
3171
+ readDefinition: (input) => requestJson2({
3172
+ apiBaseUrl,
3173
+ fetchImpl: options.fetchImpl,
3174
+ headers: managementHeaders(input),
3175
+ method: "GET",
3176
+ path: definitionPath(input),
3177
+ responseSchema: definitionContextSchema
3178
+ }),
3179
+ changeDefinition: (input) => requestJson2({
3180
+ apiBaseUrl,
3181
+ fetchImpl: options.fetchImpl,
3182
+ headers: managementHeaders(input),
3183
+ method: "PATCH",
3184
+ path: definitionPath(input),
3185
+ body: input.change,
3186
+ responseSchema: definitionChangeResponseSchema
3187
+ }),
3188
+ listSubmissions: (input) => requestJson2({
3189
+ apiBaseUrl,
3190
+ fetchImpl: options.fetchImpl,
3191
+ headers: managementHeaders(input),
3192
+ method: "GET",
3193
+ path: `${inboxPath(input)}?${input.filters}`,
3194
+ responseSchema: submissionPageSchema
3195
+ }),
3196
+ readSubmission: (input) => requestJson2({
3197
+ apiBaseUrl,
3198
+ fetchImpl: options.fetchImpl,
3199
+ headers: managementHeaders(input),
3200
+ method: "GET",
3201
+ path: `${inboxPath(input)}/${encodeURIComponent(input.submissionId)}`,
3202
+ responseSchema: submissionDetailResponseSchema
3203
+ }),
3204
+ updateSubmissionStatus: (input) => requestJson2({
3205
+ apiBaseUrl,
3206
+ fetchImpl: options.fetchImpl,
3207
+ headers: managementHeaders(input),
3208
+ method: "PATCH",
3209
+ path: `${inboxPath(input)}/${encodeURIComponent(input.submissionId)}`,
3210
+ body: { status: input.status, expectedStatus: input.expectedStatus },
3211
+ responseSchema: submissionDetailResponseSchema
3212
+ }),
3057
3213
  createProject: (input) => requestJson2({
3058
3214
  apiBaseUrl,
3059
3215
  body: { name: input.name, slug: input.slug },
@@ -3078,8 +3234,10 @@ function createSiteOSFormsApiClient(options) {
3078
3234
  fetchImpl: options.fetchImpl,
3079
3235
  headers: managementHeaders(input),
3080
3236
  method: "POST",
3081
- path: credentialCollectionPath(input),
3082
- 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
3083
3241
  }),
3084
3242
  listEnvironments: (input) => requestJson2({
3085
3243
  apiBaseUrl,
@@ -3102,7 +3260,7 @@ function createSiteOSFormsApiClient(options) {
3102
3260
  fetchImpl: options.fetchImpl,
3103
3261
  headers: managementHeaders(input),
3104
3262
  method: "GET",
3105
- path: credentialCollectionPath(input),
3263
+ path: keyCollection(input),
3106
3264
  responseSchema: formsCredentialListSchema
3107
3265
  }),
3108
3266
  revokeCredential: (input) => requestJson2({
@@ -3110,7 +3268,7 @@ function createSiteOSFormsApiClient(options) {
3110
3268
  fetchImpl: options.fetchImpl,
3111
3269
  headers: managementHeaders(input),
3112
3270
  method: "DELETE",
3113
- path: credentialItemPath(input),
3271
+ path: options.deploymentKeys ? `${keyCollection(input)}/${encodeURIComponent(input.credentialId)}` : credentialItemPath(input),
3114
3272
  responseSchema: formsCredentialMetadataSchema
3115
3273
  }),
3116
3274
  rotateCredential: (input) => requestJson2({
@@ -3120,7 +3278,9 @@ function createSiteOSFormsApiClient(options) {
3120
3278
  headers: managementHeaders(input),
3121
3279
  method: "POST",
3122
3280
  path: credentialRotatePath(input),
3123
- responseSchema: formsCredentialExchangeSchema
3281
+ responseSchema: options.deploymentKeys ? formsCredentialExchangeSchema.extend({
3282
+ token: z9.string().regex(/^pfd_[A-Za-z0-9_-]{43}$/)
3283
+ }) : formsCredentialExchangeSchema
3124
3284
  }),
3125
3285
  submitForm: (input) => requestJson2({
3126
3286
  apiBaseUrl,
@@ -3154,7 +3314,9 @@ async function requestJson2(options) {
3154
3314
  ...options.body === void 0 ? {} : { "Content-Type": "application/json" },
3155
3315
  ...options.headers
3156
3316
  },
3157
- method: options.method
3317
+ method: options.method,
3318
+ redirect: "error",
3319
+ signal: AbortSignal.timeout(3e4)
3158
3320
  });
3159
3321
  } catch {
3160
3322
  throw new SiteOSFormsApiError({
@@ -3247,7 +3409,7 @@ import {
3247
3409
  } from "fs/promises";
3248
3410
  import path11 from "path";
3249
3411
  var SITEOS_FORMS_SUBMISSION_CREDENTIAL_ENV = "SITEOS_FORMS_SUBMISSION_CREDENTIAL";
3250
- var credentialPattern = /^pfs_[A-Za-z0-9_-]{22}$/;
3412
+ var SITEOS_FORMS_DEPLOYMENT_KEY_ENV = "SITEOS_FORMS_DEPLOYMENT_KEY";
3251
3413
  var SiteOSFormsCredentialStorageError = class extends Error {
3252
3414
  code;
3253
3415
  constructor(code) {
@@ -3259,6 +3421,8 @@ var SiteOSFormsCredentialStorageError = class extends Error {
3259
3421
  }
3260
3422
  };
3261
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}$/;
3262
3426
  const projectRoot = path11.resolve(options.projectRoot);
3263
3427
  const dotenvPath = path11.join(projectRoot, ".env");
3264
3428
  try {
@@ -3285,7 +3449,7 @@ async function prepareSiteOSFormsCredentialInstallation(options) {
3285
3449
  }
3286
3450
  const currentContent = await readSafeDotenv(dotenvPath);
3287
3451
  await writeCredentialAtomically({
3288
- content: setCredential(currentContent, credential),
3452
+ content: setCredential(currentContent, credential, variable),
3289
3453
  dotenvPath,
3290
3454
  projectRoot
3291
3455
  });
@@ -3323,7 +3487,10 @@ async function readSafeDotenv(dotenvPath) {
3323
3487
  );
3324
3488
  }
3325
3489
  const content = dotenvStats ? await readFile4(dotenvPath, "utf8") : "";
3326
- if (countCredentialEntries(content) > 1) {
3490
+ if ([
3491
+ SITEOS_FORMS_SUBMISSION_CREDENTIAL_ENV,
3492
+ SITEOS_FORMS_DEPLOYMENT_KEY_ENV
3493
+ ].some((variable) => countCredentialEntries(content, variable) > 1)) {
3327
3494
  throw new SiteOSFormsCredentialStorageError(
3328
3495
  "FORMS_CREDENTIAL_STORAGE_INVALID"
3329
3496
  );
@@ -3351,21 +3518,16 @@ async function writeCredentialAtomically(options) {
3351
3518
  await rm3(temporaryPath, { force: true }).catch(() => void 0);
3352
3519
  }
3353
3520
  }
3354
- function setCredential(content, credential) {
3355
- const assignment = `${SITEOS_FORMS_SUBMISSION_CREDENTIAL_ENV}=${credential}`;
3356
- const existing = new RegExp(
3357
- `^${SITEOS_FORMS_SUBMISSION_CREDENTIAL_ENV}=[^\\r\\n]*(?=\\r?$)`,
3358
- "m"
3359
- );
3521
+ function setCredential(content, credential, variable) {
3522
+ const assignment = `${variable}=${credential}`;
3523
+ const existing = new RegExp(`^${variable}=[^\\r\\n]*(?=\\r?$)`, "m");
3360
3524
  if (existing.test(content)) return content.replace(existing, assignment);
3361
3525
  const newline = content.includes("\r\n") ? "\r\n" : "\n";
3362
3526
  if (content.length === 0) return `${assignment}${newline}`;
3363
3527
  return `${content}${content.endsWith("\n") ? "" : newline}${assignment}${newline}`;
3364
3528
  }
3365
- function countCredentialEntries(content) {
3366
- return content.split(/\r?\n/u).filter(
3367
- (line) => line.startsWith(`${SITEOS_FORMS_SUBMISSION_CREDENTIAL_ENV}=`)
3368
- ).length;
3529
+ function countCredentialEntries(content, variable) {
3530
+ return content.split(/\r?\n/u).filter((line) => line.startsWith(`${variable}=`)).length;
3369
3531
  }
3370
3532
  function isDotenvIgnored(projectRoot) {
3371
3533
  return new Promise((resolve, reject) => {
@@ -3407,25 +3569,40 @@ var SLUG_FLAG = "--slug";
3407
3569
  var CREDENTIAL_FLAG = "--credential";
3408
3570
  var REPLACE_FLAG = "--replace";
3409
3571
  var normalizedFieldOptionSchema = z10.object({
3410
- label: z10.string().trim().min(1),
3411
- value: z10.string().trim().min(1)
3572
+ label: z10.string().trim().min(1).max(255),
3573
+ value: z10.string().trim().min(1).max(255)
3412
3574
  });
3413
3575
  var normalizedFieldSchema = z10.object({
3414
- key: z10.string().trim().min(1),
3415
- kind: z10.string().trim().min(1).optional(),
3416
- label: z10.string().trim().min(1).optional(),
3576
+ key: z10.string().trim().min(1).max(255),
3577
+ kind: z10.string().trim().min(1).max(64).optional(),
3578
+ label: z10.string().trim().min(1).max(255).optional(),
3417
3579
  displayRole: z10.enum(["primary", "secondary"]).optional(),
3418
3580
  multiple: z10.boolean().optional(),
3419
- options: z10.array(normalizedFieldOptionSchema).optional()
3420
- }).passthrough();
3581
+ options: z10.array(normalizedFieldOptionSchema).max(500).optional()
3582
+ }).strict();
3421
3583
  var definitionSyncInputSchema = z10.object({
3422
- formKey: z10.string().trim().min(1),
3423
- name: z10.string().trim().min(1),
3424
- normalizedFieldsJson: z10.array(normalizedFieldSchema).min(1),
3584
+ formKey: z10.string().trim().min(1).max(255),
3585
+ name: z10.string().trim().min(1).max(255),
3586
+ normalizedFieldsJson: z10.array(normalizedFieldSchema).min(1).max(500),
3425
3587
  schemaJson: z10.record(z10.unknown()),
3426
- sourceExportId: z10.string().trim().min(1).optional(),
3427
- sourcePagePath: z10.string().trim().min(1).optional()
3428
- }).passthrough().superRefine((definition, context) => {
3588
+ sourceExportId: z10.string().trim().min(1).max(255).optional(),
3589
+ sourcePagePath: z10.string().trim().min(1).max(2048).optional()
3590
+ }).strict().superRefine((definition, context) => {
3591
+ const ajv = new Ajv({
3592
+ allErrors: true,
3593
+ removeAdditional: false,
3594
+ strict: true
3595
+ });
3596
+ addFormats.default(ajv);
3597
+ try {
3598
+ ajv.compile(definition.schemaJson);
3599
+ } catch {
3600
+ context.addIssue({
3601
+ code: z10.ZodIssueCode.custom,
3602
+ message: "schemaJson must be an executable JSON Schema supported by SiteOS Forms.",
3603
+ path: ["schemaJson"]
3604
+ });
3605
+ }
3429
3606
  const fields = definition.normalizedFieldsJson;
3430
3607
  const duplicateKeys = fields.map((field) => field.key).filter((key, index, keys) => keys.indexOf(key) !== index);
3431
3608
  for (const key of new Set(duplicateKeys)) {
@@ -3506,6 +3683,11 @@ var FORMS_HELP = `Usage:
3506
3683
  siteos forms environment create --slug <slug> --name <name> [--json]
3507
3684
  siteos forms definition sync --environment <slug> --input <path> [--json]
3508
3685
  siteos forms definition sync --environment <slug> --manifest <path> [--json]
3686
+ siteos forms definition list --environment <slug> [--status <active|inactive|all>] [--json]
3687
+ siteos forms definition read --environment <slug> --form <form-id> [--json]
3688
+ siteos forms definition archive --environment <slug> --form <form-id> --expected-revision <revision> [--json]
3689
+ siteos forms definition restore --environment <slug> --form <form-id> --expected-revision <revision> [--json]
3690
+ siteos forms definition delete --environment <slug> --form <form-id> [--apply --confirm <form-key> --expected-revision <revision> --expected-submissions <count>] [--json]
3509
3691
  siteos forms definition check --input <path> [--json]
3510
3692
  siteos forms definition check --manifest <path> [--json]
3511
3693
  siteos forms credential list --environment <slug> [--json]
@@ -3513,6 +3695,13 @@ var FORMS_HELP = `Usage:
3513
3695
  siteos forms credential rotate --environment <slug> --install [--name <name>] [--json]
3514
3696
  siteos forms credential revoke --environment <slug> --credential <credential-id> [--json]
3515
3697
  siteos forms credentials issue --environment <slug> [--name <name>] [--json]
3698
+ siteos forms submissions list --environment <slug> --form <form-id> [--query <text>] [--status <status>] [--from <ISO>] [--to <ISO>] [--limit <1-100>] [--cursor <cursor>] [--json]
3699
+ siteos forms submissions read --environment <slug> --form <form-id> --submission <id> [--json]
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]
3516
3705
  siteos forms submit --input <path> [--json]
3517
3706
 
3518
3707
  Manage SiteOS Forms Environments, definitions, credentials, and submission smoke tests.`;
@@ -3524,6 +3713,8 @@ async function runFormsCommand(options) {
3524
3713
  stdout: FORMS_HELP
3525
3714
  };
3526
3715
  }
3716
+ if (args2[0] === "deploy")
3717
+ return runDeployment({ ...options, args: args2.slice(1) });
3527
3718
  if (args2[0] !== "submit" && args2[0] !== "project") {
3528
3719
  try {
3529
3720
  args2 = await commonEnvironmentArguments(options, "forms", args2);
@@ -3534,6 +3725,8 @@ async function runFormsCommand(options) {
3534
3725
  };
3535
3726
  }
3536
3727
  }
3728
+ if (args2[0] === "submissions")
3729
+ return runSubmissionsCommand({ ...options, args: args2.slice(1) });
3537
3730
  if (args2[0] === "definition") {
3538
3731
  return runDefinitionCommand({
3539
3732
  ...options,
@@ -3558,6 +3751,13 @@ async function runFormsCommand(options) {
3558
3751
  args: args2.slice(1)
3559
3752
  });
3560
3753
  }
3754
+ if (args2[0] === "deployment-key") {
3755
+ return runCredentialCommand({
3756
+ ...options,
3757
+ args: args2.slice(1),
3758
+ deployment: true
3759
+ });
3760
+ }
3561
3761
  if (args2[0] === "credential") {
3562
3762
  return runCredentialCommand({
3563
3763
  ...options,
@@ -3577,6 +3777,96 @@ async function runFormsCommand(options) {
3577
3777
  }
3578
3778
  return usageError2("Unknown SiteOS forms command.");
3579
3779
  }
3780
+ async function runSubmissionsCommand(options) {
3781
+ const action = options.args[0];
3782
+ if (!action || !["list", "read", "status"].includes(action))
3783
+ return usageError2("Unknown Forms submissions command.");
3784
+ const parsed = parseFlags(options.args.slice(1), {
3785
+ allowed: /* @__PURE__ */ new Set([
3786
+ "--environment",
3787
+ "--form",
3788
+ "--json",
3789
+ ...action === "list" ? ["--query", "--status", "--from", "--to", "--limit", "--cursor"] : [
3790
+ "--submission",
3791
+ ...action === "status" ? ["--status", "--expected-status"] : []
3792
+ ]
3793
+ ]),
3794
+ boolean: /* @__PURE__ */ new Set(["--json"]),
3795
+ rejectDuplicates: true
3796
+ });
3797
+ if (!parsed.ok) return usageError2(parsed.error);
3798
+ if (parsed.positionals.length)
3799
+ return usageError2("Unexpected positional arguments.");
3800
+ const value = (key) => readStringFlag(parsed.flags, `--${key}`);
3801
+ const environmentSlug = value("environment");
3802
+ const formId = value("form");
3803
+ const submissionId = value("submission");
3804
+ if (!environmentSlug || !isValidEnvironmentSlug(environmentSlug) || !formId || formId.length > 255)
3805
+ return usageError2("Provide a valid --environment and --form.");
3806
+ if (action !== "list" && (!submissionId || submissionId.length > 255))
3807
+ return usageError2("Provide --submission.");
3808
+ const status = formsSubmissionStatusSchema.safeParse(value("status"));
3809
+ const expected = formsSubmissionStatusSchema.safeParse(
3810
+ value("expected-status")
3811
+ );
3812
+ if (value("status") && !status.success || action === "status" && (!status.success || !expected.success))
3813
+ return usageError2(
3814
+ "Use new, read, archived or spam for --status and --expected-status."
3815
+ );
3816
+ const filters = new URLSearchParams();
3817
+ for (const key of ["query", "status", "from", "to", "limit", "cursor"]) {
3818
+ const item = value(key);
3819
+ if (item) filters.set(key, item);
3820
+ }
3821
+ const limit = Number(value("limit") ?? 50);
3822
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100)
3823
+ return usageError2("--limit must be between 1 and 100.");
3824
+ if ((value("query")?.length ?? 0) > 200 || (value("cursor")?.length ?? 0) > 2048)
3825
+ return usageError2("The query or cursor is too long.");
3826
+ for (const key of ["from", "to"])
3827
+ if (value(key) && !z10.string().datetime({ offset: true }).safeParse(value(key)).success)
3828
+ return usageError2(
3829
+ `--${key} must be an ISO timestamp including a timezone.`
3830
+ );
3831
+ if (value("from") && value("to") && Date.parse(value("from")) >= Date.parse(value("to")))
3832
+ return usageError2("--from must be earlier than --to (exclusive).");
3833
+ const context = await loadFormsManagementContext(
3834
+ options,
3835
+ "forms:workspace:write"
3836
+ );
3837
+ if (!context.ok) return context.error;
3838
+ try {
3839
+ const { environments } = await context.client.listEnvironments({
3840
+ grant: context.grant,
3841
+ projectId: context.project.id
3842
+ });
3843
+ requireEnvironmentProjectConvergence(environments, context.project.id);
3844
+ const environment = environments.find(
3845
+ (item) => item.slug === environmentSlug
3846
+ );
3847
+ if (!environment)
3848
+ return usageError2(
3849
+ "The selected Forms Environment does not exist. Run forms environment list."
3850
+ );
3851
+ const input = {
3852
+ grant: context.grant,
3853
+ environmentId: environment.id,
3854
+ formId
3855
+ };
3856
+ const response = action === "list" ? await context.client.listSubmissions({ ...input, filters }) : action === "read" ? await context.client.readSubmission({
3857
+ ...input,
3858
+ submissionId
3859
+ }) : await context.client.updateSubmissionStatus({
3860
+ ...input,
3861
+ submissionId,
3862
+ status: status.data,
3863
+ expectedStatus: expected.data
3864
+ });
3865
+ return { exitCode: 0, stdout: stringifySafeJson(response) };
3866
+ } catch (error) {
3867
+ return formatApiError(error);
3868
+ }
3869
+ }
3580
3870
  async function runProjectCommand(options) {
3581
3871
  const action = options.args[0];
3582
3872
  if (!action || !["create", "list", "status", "use"].includes(action)) {
@@ -3803,6 +4093,11 @@ async function runCredentialCommand(options) {
3803
4093
  if (!action || !["issue", "list", "revoke", "rotate"].includes(action)) {
3804
4094
  return usageError2("Unknown SiteOS forms credential command.");
3805
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;
3806
4101
  const parsed = parseCredentialFlags(action, options.args.slice(1));
3807
4102
  if (!parsed.ok) return parsed.error;
3808
4103
  const context = await loadCredentialManagementContext({
@@ -3874,7 +4169,7 @@ Environment: ${parsed.environmentSlug}`
3874
4169
  credential,
3875
4170
  environment: parsed.environmentSlug,
3876
4171
  installed: {
3877
- variable: SITEOS_FORMS_SUBMISSION_CREDENTIAL_ENV
4172
+ variable
3878
4173
  },
3879
4174
  operation: action === "issue" ? "issued" : "rotated"
3880
4175
  };
@@ -3884,7 +4179,7 @@ Environment: ${parsed.environmentSlug}`
3884
4179
  `SiteOS Forms credential ${output.operation} and installed.`,
3885
4180
  `Credential: ${credential.id}`,
3886
4181
  `Environment: ${parsed.environmentSlug}`,
3887
- `Variable: ${SITEOS_FORMS_SUBMISSION_CREDENTIAL_ENV}`
4182
+ `Variable: ${variable}`
3888
4183
  ].join("\n")
3889
4184
  };
3890
4185
  } catch (error) {
@@ -3895,14 +4190,21 @@ async function loadCredentialManagementContext(options) {
3895
4190
  try {
3896
4191
  const local = await readLocalCommonProject(options);
3897
4192
  const rootDir = local?.rootDir ?? (await requireFormsProjectReference(options.cwd ?? process.cwd())).rootDir;
3898
- const installation = options.install ? await (options.prepareCredentialInstallation ?? prepareSiteOSFormsCredentialInstallation)({ projectRoot: rootDir }) : void 0;
4193
+ const installation = options.install ? await (options.prepareCredentialInstallation ?? prepareSiteOSFormsCredentialInstallation)({
4194
+ projectRoot: rootDir,
4195
+ ...options.deployment ? { deployment: true } : {}
4196
+ }) : void 0;
3899
4197
  const context = await loadFormsManagementContext(
3900
4198
  options,
3901
4199
  "forms:credential:manage"
3902
4200
  );
3903
4201
  if (!context.ok) return context;
3904
4202
  return {
3905
- client: context.client,
4203
+ client: options.deployment ? createSiteOSFormsApiClient({
4204
+ apiBaseUrl: context.apiBaseUrl,
4205
+ fetchImpl: options.fetchImpl ?? fetch,
4206
+ deploymentKeys: true
4207
+ }) : context.client,
3906
4208
  grant: context.grant,
3907
4209
  ...installation ? { installation } : {},
3908
4210
  ok: true,
@@ -4017,6 +4319,10 @@ async function requireFormsProjectReference(cwd) {
4017
4319
  return reference;
4018
4320
  }
4019
4321
  async function runDefinitionCommand(options) {
4322
+ if (["list", "read", "archive", "restore", "delete"].includes(
4323
+ options.args[0] ?? ""
4324
+ ))
4325
+ return runDefinitionLifecycle(options);
4020
4326
  if (options.args[0] === "sync") {
4021
4327
  return runDefinitionSync({
4022
4328
  ...options,
@@ -4031,6 +4337,125 @@ async function runDefinitionCommand(options) {
4031
4337
  }
4032
4338
  return usageError2("Unknown SiteOS forms definition command.");
4033
4339
  }
4340
+ async function runDefinitionLifecycle(options) {
4341
+ const action = options.args[0];
4342
+ const parsed = parseFlags(options.args.slice(1), {
4343
+ allowed: /* @__PURE__ */ new Set([
4344
+ "--environment",
4345
+ "--json",
4346
+ ...action === "list" ? ["--status"] : ["--form"],
4347
+ ...["archive", "restore", "delete"].includes(action) ? ["--expected-revision"] : [],
4348
+ ...action === "delete" ? ["--apply", "--confirm", "--expected-submissions"] : []
4349
+ ]),
4350
+ boolean: /* @__PURE__ */ new Set(["--json", "--apply"]),
4351
+ rejectDuplicates: true
4352
+ });
4353
+ if (!parsed.ok) return usageError2(parsed.error);
4354
+ if (parsed.positionals.length)
4355
+ return usageError2("Unexpected positional arguments.");
4356
+ const value = (key) => readStringFlag(parsed.flags, `--${key}`);
4357
+ const environmentSlug = value("environment");
4358
+ const formId = value("form");
4359
+ if (!environmentSlug || !isValidEnvironmentSlug(environmentSlug) || action !== "list" && (!formId || formId.length > 255))
4360
+ return usageError2("Provide a valid --environment and --form.");
4361
+ const status = value("status") ?? "active";
4362
+ if (!["active", "inactive", "all"].includes(status))
4363
+ return usageError2("Use active, inactive or all for --status.");
4364
+ const apply = parsed.flags.get("--apply") === true;
4365
+ const revision = Number(value("expected-revision"));
4366
+ const count = Number(value("expected-submissions"));
4367
+ const confirmKey = value("confirm");
4368
+ if ((action === "archive" || action === "restore" || apply) && (!Number.isSafeInteger(revision) || revision < 1))
4369
+ return usageError2(
4370
+ "Read the form first, then provide its --expected-revision."
4371
+ );
4372
+ if (apply && (!confirmKey || confirmKey.length > 255 || !Number.isSafeInteger(count) || count < 0))
4373
+ return usageError2(
4374
+ "Deletion requires --confirm with the exact form key and --expected-submissions from the preview."
4375
+ );
4376
+ if (action === "delete" && !apply && ["confirm", "expected-revision", "expected-submissions"].some(
4377
+ (key) => value(key) !== void 0
4378
+ ))
4379
+ return usageError2(
4380
+ "Delete is a read-only preview. Add --apply with all confirmation values to delete."
4381
+ );
4382
+ const context = await loadFormsManagementContext(
4383
+ options,
4384
+ "forms:workspace:write"
4385
+ );
4386
+ if (!context.ok) return context.error;
4387
+ try {
4388
+ const { environments } = await context.client.listEnvironments({
4389
+ grant: context.grant,
4390
+ projectId: context.project.id
4391
+ });
4392
+ requireEnvironmentProjectConvergence(environments, context.project.id);
4393
+ const environment = environments.find(
4394
+ (item) => item.slug === environmentSlug
4395
+ );
4396
+ if (!environment)
4397
+ return usageError2(
4398
+ "The selected Forms Environment does not exist. Run forms environment list."
4399
+ );
4400
+ const input = {
4401
+ grant: context.grant,
4402
+ environmentId: environment.id,
4403
+ formId: formId ?? ""
4404
+ };
4405
+ if (action === "list")
4406
+ return {
4407
+ exitCode: 0,
4408
+ stdout: stringifySafeJson(
4409
+ await context.client.listDefinitions({
4410
+ ...input,
4411
+ status
4412
+ })
4413
+ )
4414
+ };
4415
+ if (action === "read" || action === "delete" && !apply) {
4416
+ const preview = await context.client.readDefinition(input);
4417
+ if (preview.project.id !== context.project.id || preview.environment.id !== environment.id || preview.definition.id !== formId || preview.definition.environmentId !== environment.id)
4418
+ throw new SiteOSFormsApiError({
4419
+ code: "INVALID_RESPONSE",
4420
+ message: "The form does not match the selected Project and Environment."
4421
+ });
4422
+ return {
4423
+ exitCode: 0,
4424
+ stdout: stringifySafeJson(
4425
+ action === "read" ? preview : {
4426
+ ...preview,
4427
+ action: "delete",
4428
+ applied: false,
4429
+ warning: "Permanently removes this form, all versions and all submitted answers. This cannot be undone. The form key remains reserved.",
4430
+ confirmation: {
4431
+ confirmKey: preview.definition.formKey,
4432
+ expectedRevision: preview.revision,
4433
+ expectedSubmissionCount: preview.definition.submissionCount
4434
+ }
4435
+ }
4436
+ )
4437
+ };
4438
+ }
4439
+ const response = await context.client.changeDefinition({
4440
+ ...input,
4441
+ change: action === "delete" ? {
4442
+ action,
4443
+ confirmKey,
4444
+ expectedRevision: revision,
4445
+ expectedSubmissionCount: count
4446
+ } : {
4447
+ action,
4448
+ expectedRevision: revision
4449
+ }
4450
+ });
4451
+ return {
4452
+ exitCode: 0,
4453
+ stdout: stringifySafeJson({ ...response, action, applied: true })
4454
+ };
4455
+ } catch (error) {
4456
+ return formatApiError(error);
4457
+ }
4458
+ }
4034
4459
  async function runDefinitionSync(options) {
4035
4460
  const parsed = parseDefinitionSourceFlags(options.args, {
4036
4461
  environmentRequired: true,
@@ -4713,8 +5138,8 @@ function formatApiError(error) {
4713
5138
  error.status ? `Status: ${error.status}` : void 0,
4714
5139
  `Code: ${error.code}`,
4715
5140
  error.message,
4716
- error.status === 404 && error.code === "NOT_FOUND" ? "The form was not found. Run definition sync before testing submissions." : void 0,
4717
- error.status === 409 && error.code === "PROJECT_FORM_SUBMISSION_EXISTS" ? "The submission idempotency key was already used. Retry with a new idempotency key." : void 0
5141
+ error.status === 404 && error.code === "NOT_FOUND" ? "Verify the selected Project, Environment and form ID. The server must support this command." : void 0,
5142
+ error.status === 409 && error.code === "PROJECT_FORM_SUBMISSION_EXISTS" ? "The submission idempotency key was already used. Keep it for an unchanged retry and check the existing receipt before sending again." : void 0
4718
5143
  ].filter((line) => Boolean(line));
4719
5144
  return {
4720
5145
  exitCode: 1,
@@ -4883,6 +5308,55 @@ function createFormsUiOutput(input) {
4883
5308
  url
4884
5309
  };
4885
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
+ }
4886
5360
 
4887
5361
  // src/package-metadata.ts
4888
5362
  import { readFileSync } from "fs";
@@ -4927,7 +5401,7 @@ var projectSchema = z11.object({
4927
5401
  }).strict();
4928
5402
  var projectListSchema = z11.object({ projects: z11.array(projectSchema) }).strict();
4929
5403
  var projectResponseSchema = z11.object({ project: projectSchema }).strict();
4930
- var deploymentResponseSchema = z11.object({
5404
+ var deploymentResponseSchema2 = z11.object({
4931
5405
  checks: z11.array(
4932
5406
  z11.object({
4933
5407
  checkId: z11.string().min(1),
@@ -5032,7 +5506,7 @@ function createPulseApiClient(input) {
5032
5506
  grant: options.grant,
5033
5507
  method: "POST",
5034
5508
  path: "/api/cli/v1/deployments",
5035
- schema: deploymentResponseSchema
5509
+ schema: deploymentResponseSchema2
5036
5510
  });
5037
5511
  }
5038
5512
  };