@cowliss/cli 0.5.0 → 0.6.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 +3 -4
- package/dist/guest/{driver-14FnzM-h.js → driver-DBt5l1Z5.js} +4 -3
- package/dist/guest/driver.d.ts +1 -1
- package/dist/guest/driver.js +1 -1
- package/dist/guest/{index-BDj02EsQ.d.ts → index-CXcCEcAg.d.ts} +7 -4
- package/dist/guest/{journeys-C8j8Sqvl.js → journeys-BrcKEXz0.js} +70 -51
- package/dist/guest/journeys.d.ts +22 -18
- package/dist/guest/journeys.js +1 -1
- package/dist/guest/wasi.js +1 -1
- package/dist/index.js +766 -736
- package/examples/abandoned-checkout/journeys/abandoned-checkout.ts +0 -1
- package/examples/abandoned-checkout/scenarios/abandoned-checkout.timeout.json +1 -2
- package/examples/activity-decay/journeys/activity-decay.ts +0 -1
- package/examples/cross-app-pitch/journeys/cross-app-pitch.ts +4 -4
- package/examples/cross-app-pitch/scenarios/cross-app-pitch.json +1 -1
- package/examples/winback/journeys/winback.ts +0 -1
- package/examples/winback/scenarios/winback.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -38,24 +38,6 @@ const REQUEST_ID_HEADER = "X-Request-Id";
|
|
|
38
38
|
*/
|
|
39
39
|
const CLERK_ORG_SUBJECT_PREFIX = "org_";
|
|
40
40
|
/**
|
|
41
|
-
* The two fixed environments every org has (spec: Environments). An app
|
|
42
|
-
* belongs to one, so the environment of every write is the app's;
|
|
43
|
-
* profiles, identifiers, events, memberships, journey instances,
|
|
44
|
-
* deliveries, violations, quarantine, the address ledger, and idempotency
|
|
45
|
-
* keys are per environment, while the catalog, sending domains, the
|
|
46
|
-
* suppression mirror, billing, and journey code are shared. A third
|
|
47
|
-
* environment is a one-line change here plus the mirrored db enum.
|
|
48
|
-
*/
|
|
49
|
-
const ENVIRONMENTS = ["development", "production"];
|
|
50
|
-
/** What an admin call gets when it names no environment. */
|
|
51
|
-
const DEFAULT_ENVIRONMENT = "production";
|
|
52
|
-
/**
|
|
53
|
-
* The request header admin endpoints read the environment from; absent
|
|
54
|
-
* means production. Ingestion endpoints ignore it: the environment of a
|
|
55
|
-
* write comes from the app.
|
|
56
|
-
*/
|
|
57
|
-
const ENVIRONMENT_HEADER = "X-Cow-Environment";
|
|
58
|
-
/**
|
|
59
41
|
* The header every Cowliss client names itself in, as
|
|
60
42
|
* `<package>/<version>` (`@cowliss/cli/0.1.0`). The API logs it and reads
|
|
61
43
|
* nothing from it: no behaviour depends on the value, so a caller that
|
|
@@ -74,16 +56,6 @@ const ENVIRONMENT_HEADER = "X-Cow-Environment";
|
|
|
74
56
|
*/
|
|
75
57
|
const CLIENT_HEADER = "X-Cow-Client";
|
|
76
58
|
/**
|
|
77
|
-
* Event retention per environment, in days: both the default (what an org
|
|
78
|
-
* gets without setting anything) and the cap (the API refuses more). There
|
|
79
|
-
* is no "forever". Production keeps 18 months; development keeps 30 days,
|
|
80
|
-
* because test traffic is disposable by definition.
|
|
81
|
-
*/
|
|
82
|
-
const EVENT_RETENTION_DAYS = {
|
|
83
|
-
production: 548,
|
|
84
|
-
development: 30
|
|
85
|
-
};
|
|
86
|
-
/**
|
|
87
59
|
* The named identifiers a call may carry, a fixed enum grown by code (spec:
|
|
88
60
|
* Identity). `anonymousId` is reserved only in that a profile whose
|
|
89
61
|
* identifiers are all anonymous is labelled anonymous in the dashboard;
|
|
@@ -3084,21 +3056,6 @@ const abuseEvents = pgTable("abuse_events", {
|
|
|
3084
3056
|
const selectAbuseEventSchema = createSelectSchema(abuseEvents);
|
|
3085
3057
|
const insertAbuseEventSchema = createInsertSchema(abuseEvents);
|
|
3086
3058
|
|
|
3087
|
-
//#endregion
|
|
3088
|
-
//#region ../../packages/db/src/schema/environments.ts
|
|
3089
|
-
/**
|
|
3090
|
-
* The two fixed environments every org has. A partition inside the org, not
|
|
3091
|
-
* a resource: no table, no ids, no create surface. The environment of a
|
|
3092
|
-
* write comes from the app it names (apps carry one), and everything
|
|
3093
|
-
* downstream of a write (profiles, identifiers, events, memberships,
|
|
3094
|
-
* deliveries, runs, violations, quarantine, the address ledger) is stamped
|
|
3095
|
-
* with it.
|
|
3096
|
-
*
|
|
3097
|
-
* The literal list mirrors ENVIRONMENTS in packages/shared/constants; db
|
|
3098
|
-
* cannot import it without closing a package cycle (shared -> db).
|
|
3099
|
-
*/
|
|
3100
|
-
const environmentEnum = pgEnum("environment", ["development", "production"]);
|
|
3101
|
-
|
|
3102
3059
|
//#endregion
|
|
3103
3060
|
//#region ../../packages/db/src/schema/timestamps.ts
|
|
3104
3061
|
/**
|
|
@@ -3134,12 +3091,7 @@ const updatedAt = () => stamp("updated_at").notNull().defaultNow().$onUpdate(()
|
|
|
3134
3091
|
/**
|
|
3135
3092
|
* Apps: the registry of an org's apps/products, and the attribution unit.
|
|
3136
3093
|
* Everything downstream (ingestion, via the source a payload names;
|
|
3137
|
-
* segments, journeys,
|
|
3138
|
-
*
|
|
3139
|
-
* An app belongs to exactly one environment, chosen at creation and
|
|
3140
|
-
* immutable: the environment of every write is the environment of the
|
|
3141
|
-
* app behind the source the payload names, and nothing else carries it.
|
|
3142
|
-
* The dev and prod rows of one app are two rows with two names and two ids.
|
|
3094
|
+
* segments, journeys, webhooks) validates appIds against this table.
|
|
3143
3095
|
*
|
|
3144
3096
|
* The id is readable, `app_<slug>` derived from the name at creation, and
|
|
3145
3097
|
* unique per org rather than globally: the primary key is (orgId, id), and
|
|
@@ -3162,7 +3114,6 @@ const appStatusEnum = pgEnum("app_status", ["active", "archived"]);
|
|
|
3162
3114
|
const apps$1 = pgTable("apps", {
|
|
3163
3115
|
id: text("id").notNull(),
|
|
3164
3116
|
orgId: text("org_id").notNull(),
|
|
3165
|
-
environment: environmentEnum("environment").notNull(),
|
|
3166
3117
|
name: text("name").notNull(),
|
|
3167
3118
|
status: appStatusEnum("status").notNull().default("active"),
|
|
3168
3119
|
createdAt: createdAt(),
|
|
@@ -3317,7 +3268,7 @@ const insertCatalogTraitSchema = createInsertSchema(catalogTraits);
|
|
|
3317
3268
|
* in their `cow.json`. One read here returns the whole set, so nothing
|
|
3318
3269
|
* downstream unions a table with a constant.
|
|
3319
3270
|
*
|
|
3320
|
-
* Org-wide rather than per-project
|
|
3271
|
+
* Org-wide rather than per-project: `profiles.consent` is
|
|
3321
3272
|
* one jsonb map per profile, so an answer given under one project is the
|
|
3322
3273
|
* same answer under the next, and two projects declaring one key must agree
|
|
3323
3274
|
* on its label and default or the deploy is refused.
|
|
@@ -3367,8 +3318,16 @@ const deliveryChannelEnum = pgEnum("delivery_channel", ["email", "webhook"]);
|
|
|
3367
3318
|
* nullable. The meter and the quota gate exclude it (packages/delivery's
|
|
3368
3319
|
* repository), and every per-profile read excludes it by construction,
|
|
3369
3320
|
* because it has no profile to be found under.
|
|
3321
|
+
*
|
|
3322
|
+
* `api` is a direct send through the Resend-compatible facade (ADR 0014):
|
|
3323
|
+
* no journey and no profile either, addressed to whatever the caller named,
|
|
3324
|
+
* and metered exactly like a journey's email.
|
|
3370
3325
|
*/
|
|
3371
|
-
const deliveryKindEnum = pgEnum("delivery_kind", [
|
|
3326
|
+
const deliveryKindEnum = pgEnum("delivery_kind", [
|
|
3327
|
+
"journey",
|
|
3328
|
+
"test",
|
|
3329
|
+
"api"
|
|
3330
|
+
]);
|
|
3372
3331
|
/**
|
|
3373
3332
|
* The delivery lifecycle, frozen with the API version.
|
|
3374
3333
|
*
|
|
@@ -3400,7 +3359,6 @@ const deliveryStatusEnum = pgEnum("delivery_status", [
|
|
|
3400
3359
|
"skipped_quota",
|
|
3401
3360
|
"skipped_frequency_cap",
|
|
3402
3361
|
"skipped_consent",
|
|
3403
|
-
"skipped_sender",
|
|
3404
3362
|
"skipped_domain",
|
|
3405
3363
|
"skipped_ssrf",
|
|
3406
3364
|
"would_send",
|
|
@@ -3410,34 +3368,47 @@ const deliveryStatusEnum = pgEnum("delivery_status", [
|
|
|
3410
3368
|
"would_skip_quota",
|
|
3411
3369
|
"would_skip_frequency_cap",
|
|
3412
3370
|
"would_skip_consent",
|
|
3413
|
-
"would_skip_sender",
|
|
3414
3371
|
"would_skip_domain",
|
|
3415
3372
|
"would_skip_ssrf"
|
|
3416
3373
|
]);
|
|
3417
3374
|
const deliveries$1 = pgTable("deliveries", {
|
|
3418
3375
|
id: text("id").primaryKey(),
|
|
3419
3376
|
orgId: text("org_id").notNull(),
|
|
3420
|
-
/** The instance's environment, stamped from the workflow run input. */
|
|
3421
|
-
environment: environmentEnum("environment").notNull(),
|
|
3422
3377
|
/**
|
|
3423
3378
|
* The recipient profile (`usr_`); a text column, since a delivery is a
|
|
3424
3379
|
* log row and outlives nothing but erasure. Null on a test send, which
|
|
3425
|
-
* goes to the member who asked for it rather than to anyone's profile
|
|
3380
|
+
* goes to the member who asked for it rather than to anyone's profile,
|
|
3381
|
+
* and on an api send, which names addresses and knows no profile.
|
|
3426
3382
|
*/
|
|
3427
3383
|
profileId: text("profile_id"),
|
|
3428
|
-
/**
|
|
3384
|
+
/**
|
|
3385
|
+
* The journey that sent this; null on a test send and on an api send,
|
|
3386
|
+
* neither of which has one.
|
|
3387
|
+
*/
|
|
3429
3388
|
journey: text("journey"),
|
|
3430
3389
|
kind: deliveryKindEnum("kind").notNull().default("journey"),
|
|
3431
|
-
/**
|
|
3390
|
+
/**
|
|
3391
|
+
* Email template name or webhook name. An api send that named no
|
|
3392
|
+
* template says `email`: there is a message but no key to point at.
|
|
3393
|
+
*/
|
|
3432
3394
|
step: text("step").notNull(),
|
|
3433
3395
|
channel: deliveryChannelEnum("channel").notNull(),
|
|
3434
3396
|
status: deliveryStatusEnum("status").notNull(),
|
|
3435
3397
|
dryRun: boolean("dry_run").notNull().default(false),
|
|
3436
3398
|
/**
|
|
3437
|
-
* Recipient email address or
|
|
3438
|
-
*
|
|
3399
|
+
* Recipient email address or webhook URL. An api send to several
|
|
3400
|
+
* addresses records the first `to`; the rest are in the payload. PII:
|
|
3401
|
+
* the logger redacts this path, so it is stored here and nowhere else.
|
|
3439
3402
|
*/
|
|
3440
3403
|
recipient: text("recipient"),
|
|
3404
|
+
/**
|
|
3405
|
+
* What the send was made of, per kind: a journey or test email holds the
|
|
3406
|
+
* template props (plus `rendered` on a dry run or a test), a webhook
|
|
3407
|
+
* holds its body, and an `api` send holds the caller's envelope as they
|
|
3408
|
+
* spelled it, `{ from, to, cc, bcc, reply_to, subject, headers,
|
|
3409
|
+
* template, variables }`. Never the rendered bodies of a real send: SES
|
|
3410
|
+
* has the message and the log does not keep a second copy.
|
|
3411
|
+
*/
|
|
3441
3412
|
payload: jsonb("payload").$type().notNull().default({}),
|
|
3442
3413
|
/**
|
|
3443
3414
|
* The provider's message id (SES's MessageId), stored so a feedback
|
|
@@ -3470,7 +3441,7 @@ const deliveries$1 = pgTable("deliveries", {
|
|
|
3470
3441
|
precision: 3
|
|
3471
3442
|
})
|
|
3472
3443
|
}, (table) => [
|
|
3473
|
-
index("
|
|
3444
|
+
index("deliveries_org_id_created_at_idx").on(table.orgId, table.createdAt),
|
|
3474
3445
|
index("deliveries_org_id_profile_id_created_at_idx").on(table.orgId, table.profileId, table.createdAt),
|
|
3475
3446
|
uniqueIndex("deliveries_org_id_attempt_key_unique").on(table.orgId, table.attemptKey),
|
|
3476
3447
|
uniqueIndex("deliveries_provider_id_unique").on(table.providerId),
|
|
@@ -3479,62 +3450,14 @@ const deliveries$1 = pgTable("deliveries", {
|
|
|
3479
3450
|
const selectDeliverySchema = createSelectSchema(deliveries$1);
|
|
3480
3451
|
const insertDeliverySchema = createInsertSchema(deliveries$1);
|
|
3481
3452
|
|
|
3482
|
-
//#endregion
|
|
3483
|
-
//#region ../../packages/db/src/schema/destinations.ts
|
|
3484
|
-
/**
|
|
3485
|
-
* Destinations: named send targets (webhook URLs, sender identities) that
|
|
3486
|
-
* journeys reference by name. Mirrors the apps table conventions: ms
|
|
3487
|
-
* precision timestamps for cursor round-trips, one environment chosen at
|
|
3488
|
-
* creation and immutable, and name uniqueness per org AND environment, so
|
|
3489
|
-
* `crm` can point at the staging receiver in development and at the real one
|
|
3490
|
-
* in production. A journey step naming `webhook("crm")` resolves to the row
|
|
3491
|
-
* in the instance's environment; the same rule picks the sender identity, of
|
|
3492
|
-
* which an org has one per environment.
|
|
3493
|
-
*
|
|
3494
|
-
* WEBHOOK_PAYLOAD_VERSION (packages/shared) pins the webhook payload shape;
|
|
3495
|
-
* the column records the version each destination was created against.
|
|
3496
|
-
* signingSecret is the Standard Webhooks signing secret, generated at
|
|
3497
|
-
* creation and shown once — the API DTO omits it after the create response.
|
|
3498
|
-
*/
|
|
3499
|
-
const destinationTypeEnum = pgEnum("destination_type", ["webhook", "sender_identity"]);
|
|
3500
|
-
const destinations$1 = pgTable("destinations", {
|
|
3501
|
-
id: text("id").primaryKey(),
|
|
3502
|
-
orgId: text("org_id").notNull(),
|
|
3503
|
-
environment: environmentEnum("environment").notNull(),
|
|
3504
|
-
name: text("name").notNull(),
|
|
3505
|
-
type: destinationTypeEnum("type").notNull(),
|
|
3506
|
-
config: jsonb("config").$type().notNull(),
|
|
3507
|
-
webhookPayloadVersion: integer("webhook_payload_version").notNull().default(1),
|
|
3508
|
-
signingSecret: text("signing_secret"),
|
|
3509
|
-
/**
|
|
3510
|
-
* Auto-disable state, mirroring how receivers treat us: the webhook send
|
|
3511
|
-
* activity bumps the counter once per failed delivery attempt (not once
|
|
3512
|
-
* per Temporal retry) and stamps disabledAt at the threshold. A disabled
|
|
3513
|
-
* destination records `skipped_disabled` until an admin clears it; a
|
|
3514
|
-
* successful delivery resets the counter.
|
|
3515
|
-
*/
|
|
3516
|
-
consecutiveFailures: integer("consecutive_failures").notNull().default(0),
|
|
3517
|
-
disabledAt: timestamp("disabled_at", {
|
|
3518
|
-
withTimezone: true,
|
|
3519
|
-
mode: "date",
|
|
3520
|
-
precision: 3
|
|
3521
|
-
}),
|
|
3522
|
-
createdAt: createdAt(),
|
|
3523
|
-
updatedAt: updatedAt()
|
|
3524
|
-
}, (table) => [index("destinations_org_id_idx").on(table.orgId), uniqueIndex("destinations_org_id_environment_name_unique").on(table.orgId, table.environment, table.name)]);
|
|
3525
|
-
const selectDestinationSchema = createSelectSchema(destinations$1);
|
|
3526
|
-
const insertDestinationSchema = createInsertSchema(destinations$1);
|
|
3527
|
-
|
|
3528
3453
|
//#endregion
|
|
3529
3454
|
//#region ../../packages/db/src/schema/email-addresses.ts
|
|
3530
3455
|
/**
|
|
3531
3456
|
* The email address ledger (ticket 35): everything Cowliss knows about an
|
|
3532
|
-
* address, scoped to the org and
|
|
3533
|
-
*
|
|
3534
|
-
*
|
|
3535
|
-
*
|
|
3536
|
-
* it. Per environment because a verification proven against a development
|
|
3537
|
-
* profile says nothing about production.
|
|
3457
|
+
* address, scoped to the org and keyed by the address rather than the
|
|
3458
|
+
* profile. Verification is address-scoped by nature: a profile that changes
|
|
3459
|
+
* its email has not verified the new one, and an address verified once
|
|
3460
|
+
* stays verified when the same profile comes back to it.
|
|
3538
3461
|
*
|
|
3539
3462
|
* The profile keeps its canonical `email` trait as the address Cowliss sends
|
|
3540
3463
|
* to, plus the system-maintained `emailVerifiedAt` / `emailVerifiedAddress`
|
|
@@ -3552,7 +3475,6 @@ const verificationMethodEnum = pgEnum("verification_method", [
|
|
|
3552
3475
|
const emailAddresses = pgTable("email_addresses", {
|
|
3553
3476
|
id: text("id").primaryKey(),
|
|
3554
3477
|
orgId: text("org_id").notNull(),
|
|
3555
|
-
environment: environmentEnum("environment").notNull(),
|
|
3556
3478
|
/** Stored lowercased and trimmed: the mailbox, not the way it was typed. */
|
|
3557
3479
|
address: text("address").notNull(),
|
|
3558
3480
|
/** Null until something verified the address. */
|
|
@@ -3564,45 +3486,10 @@ const emailAddresses = pgTable("email_addresses", {
|
|
|
3564
3486
|
verificationMethod: verificationMethodEnum("verification_method"),
|
|
3565
3487
|
createdAt: createdAt(),
|
|
3566
3488
|
updatedAt: updatedAt()
|
|
3567
|
-
}, (table) => [uniqueIndex("
|
|
3489
|
+
}, (table) => [uniqueIndex("email_addresses_org_address_unique").on(table.orgId, table.address)]);
|
|
3568
3490
|
const selectEmailAddressSchema = createSelectSchema(emailAddresses);
|
|
3569
3491
|
const insertEmailAddressSchema = createInsertSchema(emailAddresses);
|
|
3570
3492
|
|
|
3571
|
-
//#endregion
|
|
3572
|
-
//#region ../../packages/db/src/schema/environment-settings.ts
|
|
3573
|
-
/**
|
|
3574
|
-
* The ingestion policy for catalog governance. Single source of truth:
|
|
3575
|
-
* @cowliss/shared/governance derives INGESTION_POLICIES from
|
|
3576
|
-
* ingestionPolicyEnum.enumValues, so the enum and the settings contract
|
|
3577
|
-
* cannot drift.
|
|
3578
|
-
*/
|
|
3579
|
-
const ingestionPolicyEnum = pgEnum("ingestion_policy", ["permissive", "strict"]);
|
|
3580
|
-
/**
|
|
3581
|
-
* Per-environment settings, keyed by org and environment. "No row means
|
|
3582
|
-
* every default applies", the same convention as org_settings: a fresh org
|
|
3583
|
-
* is zero-config in both environments, and the write path never seeds rows
|
|
3584
|
-
* it does not own.
|
|
3585
|
-
*
|
|
3586
|
-
* `ingestionPolicy`: permissive accepts quarantined (unclassified) names and
|
|
3587
|
-
* observes them; strict routes them to the review queue instead of applying
|
|
3588
|
-
* them. Development can be strict while production stays lenient.
|
|
3589
|
-
*
|
|
3590
|
-
* `eventRetentionDays`: the ClickHouse TTL window for the environment's
|
|
3591
|
-
* events. Null means the environment's default (18 months in production,
|
|
3592
|
-
* 30 days in development, from packages/shared constants); there is no
|
|
3593
|
-
* "forever", and the API refuses a value above the environment's cap.
|
|
3594
|
-
*/
|
|
3595
|
-
const environmentSettings = pgTable("environment_settings", {
|
|
3596
|
-
orgId: text("org_id").notNull(),
|
|
3597
|
-
environment: environmentEnum("environment").notNull(),
|
|
3598
|
-
ingestionPolicy: ingestionPolicyEnum("ingestion_policy").notNull().default("permissive"),
|
|
3599
|
-
eventRetentionDays: integer("event_retention_days"),
|
|
3600
|
-
createdAt: createdAt(),
|
|
3601
|
-
updatedAt: updatedAt()
|
|
3602
|
-
}, (table) => [primaryKey({ columns: [table.orgId, table.environment] })]);
|
|
3603
|
-
const selectEnvironmentSettingsSchema = createSelectSchema(environmentSettings);
|
|
3604
|
-
const insertEnvironmentSettingsSchema = createInsertSchema(environmentSettings);
|
|
3605
|
-
|
|
3606
3493
|
//#endregion
|
|
3607
3494
|
//#region ../../packages/db/src/schema/executions.ts
|
|
3608
3495
|
const executionStatusEnum = pgEnum("execution_status", [
|
|
@@ -3615,7 +3502,6 @@ const executionStatusEnum = pgEnum("execution_status", [
|
|
|
3615
3502
|
const executions$1 = pgTable("executions", {
|
|
3616
3503
|
id: text("id").primaryKey(),
|
|
3617
3504
|
orgId: text("org_id").notNull(),
|
|
3618
|
-
environment: environmentEnum("environment").notNull(),
|
|
3619
3505
|
/** The journey's key, not a foreign key: derived journey rows come and go with pushes. */
|
|
3620
3506
|
journeyKey: text("journey_key").notNull(),
|
|
3621
3507
|
profileId: text("profile_id").notNull(),
|
|
@@ -3664,9 +3550,9 @@ const executions$1 = pgTable("executions", {
|
|
|
3664
3550
|
precision: 3
|
|
3665
3551
|
})
|
|
3666
3552
|
}, (table) => [
|
|
3667
|
-
index("
|
|
3553
|
+
index("executions_org_id_version_id_status_idx").on(table.orgId, table.versionId, table.status),
|
|
3668
3554
|
uniqueIndex("executions_org_id_workflow_id_unique").on(table.orgId, table.workflowId),
|
|
3669
|
-
index("
|
|
3555
|
+
index("executions_org_id_started_at_idx").on(table.orgId, table.startedAt)
|
|
3670
3556
|
]);
|
|
3671
3557
|
const selectExecutionSchema = createSelectSchema(executions$1);
|
|
3672
3558
|
const insertExecutionSchema = createInsertSchema(executions$1);
|
|
@@ -3725,7 +3611,7 @@ const idempotencyKeys = pgTable("idempotency_keys", {
|
|
|
3725
3611
|
//#endregion
|
|
3726
3612
|
//#region ../../packages/db/src/schema/profiles.ts
|
|
3727
3613
|
/**
|
|
3728
|
-
* Profiles: one row per person
|
|
3614
|
+
* Profiles: one row per person. Cowliss generates the id
|
|
3729
3615
|
* (`usr_`) and that id is the person key everywhere: workflow ids,
|
|
3730
3616
|
* ClickHouse rows, segment membership, deliveries, journey runs, erasure.
|
|
3731
3617
|
* Callers never send it on the write path; they send named identifiers
|
|
@@ -3739,10 +3625,6 @@ const idempotencyKeys = pgTable("idempotency_keys", {
|
|
|
3739
3625
|
* profile exists under at all. It is written by the identify path (a caller
|
|
3740
3626
|
* passing `consent`), the consent editor, and the automatic revocations.
|
|
3741
3627
|
*
|
|
3742
|
-
* `environment` is the app's, stamped at creation and never changed: a
|
|
3743
|
-
* person in development and a person in production are two rows even when
|
|
3744
|
-
* their identifiers match.
|
|
3745
|
-
*
|
|
3746
3628
|
* `mergedInto` is the merge pointer (spec: Identity). Null on a live
|
|
3747
3629
|
* profile; on a profile a merge folded away it names the survivor, whose
|
|
3748
3630
|
* reads union its merged ids. The graph stays flat: a merge re-points rows
|
|
@@ -3756,7 +3638,6 @@ const idempotencyKeys = pgTable("idempotency_keys", {
|
|
|
3756
3638
|
const profiles = pgTable("profiles", {
|
|
3757
3639
|
id: text("id").primaryKey(),
|
|
3758
3640
|
orgId: text("org_id").notNull(),
|
|
3759
|
-
environment: environmentEnum("environment").notNull(),
|
|
3760
3641
|
appId: text("app_id").notNull(),
|
|
3761
3642
|
sourceId: text("source_id").notNull(),
|
|
3762
3643
|
traits: jsonb("traits").$type().notNull().default({}),
|
|
@@ -3768,7 +3649,7 @@ const profiles = pgTable("profiles", {
|
|
|
3768
3649
|
createdAt: createdAt(),
|
|
3769
3650
|
updatedAt: updatedAt()
|
|
3770
3651
|
}, (table) => [
|
|
3771
|
-
index("
|
|
3652
|
+
index("profiles_org_id_idx").on(table.orgId),
|
|
3772
3653
|
index("profiles_merged_into_idx").on(table.mergedInto),
|
|
3773
3654
|
foreignKey({
|
|
3774
3655
|
columns: [table.mergedInto],
|
|
@@ -3804,10 +3685,10 @@ const identifierKindEnum = pgEnum("identifier_kind", [
|
|
|
3804
3685
|
]);
|
|
3805
3686
|
/**
|
|
3806
3687
|
* The identifiers map: every named identifier a call has ever carried,
|
|
3807
|
-
* pointing at the profile it resolved to. Unique per (org,
|
|
3808
|
-
*
|
|
3809
|
-
*
|
|
3810
|
-
*
|
|
3688
|
+
* pointing at the profile it resolved to. Unique per (org, kind, value),
|
|
3689
|
+
* which is what makes "a shared identifier means the same person"
|
|
3690
|
+
* enforceable at the row level: the same clerkId twice in one organization
|
|
3691
|
+
* is one person.
|
|
3811
3692
|
*
|
|
3812
3693
|
* Rows follow their profile: erasure deletes the profile and these cascade,
|
|
3813
3694
|
* and a merge re-points them at the survivor.
|
|
@@ -3815,12 +3696,11 @@ const identifierKindEnum = pgEnum("identifier_kind", [
|
|
|
3815
3696
|
const identifiers = pgTable("identifiers", {
|
|
3816
3697
|
id: text("id").primaryKey(),
|
|
3817
3698
|
orgId: text("org_id").notNull(),
|
|
3818
|
-
environment: environmentEnum("environment").notNull(),
|
|
3819
3699
|
kind: identifierKindEnum("kind").notNull(),
|
|
3820
3700
|
value: text("value").notNull(),
|
|
3821
3701
|
profileId: text("profile_id").notNull().references(() => profiles.id, { onDelete: "cascade" }),
|
|
3822
3702
|
createdAt: createdAt()
|
|
3823
|
-
}, (table) => [uniqueIndex("
|
|
3703
|
+
}, (table) => [uniqueIndex("identifiers_org_kind_value_unique").on(table.orgId, table.kind, table.value), index("identifiers_profile_id_idx").on(table.profileId)]);
|
|
3824
3704
|
const selectIdentifierSchema = createSelectSchema(identifiers);
|
|
3825
3705
|
const insertIdentifierSchema = createInsertSchema(identifiers);
|
|
3826
3706
|
|
|
@@ -3846,8 +3726,6 @@ const insertIdentifierSchema = createInsertSchema(identifiers);
|
|
|
3846
3726
|
const journeyRuns = pgTable("journey_runs", {
|
|
3847
3727
|
id: text("id").primaryKey(),
|
|
3848
3728
|
orgId: text("org_id").notNull(),
|
|
3849
|
-
/** The instance's environment; development instances never land here (they bill nothing). */
|
|
3850
|
-
environment: environmentEnum("environment").notNull(),
|
|
3851
3729
|
/** The journey's name, not its registry id: the run is about the definition that ran. */
|
|
3852
3730
|
journey: text("journey").notNull(),
|
|
3853
3731
|
profileId: text("profile_id").notNull(),
|
|
@@ -3872,10 +3750,6 @@ const insertJourneyRunSchema = createInsertSchema(journeyRuns);
|
|
|
3872
3750
|
* there is no id of its own: the key is the name the author gave the file,
|
|
3873
3751
|
* and the (org, key) pair is the only identity a journey has.
|
|
3874
3752
|
*
|
|
3875
|
-
* One row for both environments (ADR 0011): the journey's code is its
|
|
3876
|
-
* latest ready version, and what differs per environment is the enabled
|
|
3877
|
-
* flag alone. A trigger that should fire in both names both of an app's ids.
|
|
3878
|
-
*
|
|
3879
3753
|
* A push only ever adds and updates: it owns the keys its manifest carries
|
|
3880
3754
|
* and leaves every other row alone, because an org's journeys can come from
|
|
3881
3755
|
* several projects and a developer may push a project holding only some of
|
|
@@ -3883,9 +3757,9 @@ const insertJourneyRunSchema = createInsertSchema(journeyRuns);
|
|
|
3883
3757
|
* what makes a key another project owns a refused push rather than a silent
|
|
3884
3758
|
* overwrite. Removing a journey is an explicit delete (ADR 0009).
|
|
3885
3759
|
*
|
|
3886
|
-
* Whether a journey fires is one flag and one only:
|
|
3887
|
-
*
|
|
3888
|
-
*
|
|
3760
|
+
* Whether a journey fires is one flag and one only: `enabled`, which a
|
|
3761
|
+
* push never writes, so deploying code never turns anything on or off. The
|
|
3762
|
+
* author has no second gate of their own (ADR 0011).
|
|
3889
3763
|
*
|
|
3890
3764
|
* The descriptive columns are what its latest ready version reported, so a
|
|
3891
3765
|
* failed compile leaves both the code and its description as they were.
|
|
@@ -3909,47 +3783,35 @@ const journeys$1 = pgTable("journeys", {
|
|
|
3909
3783
|
*/
|
|
3910
3784
|
purpose: text("purpose").notNull(),
|
|
3911
3785
|
spine: jsonb("spine").$type().notNull(),
|
|
3786
|
+
/**
|
|
3787
|
+
* The one gate on the journey: off until `cow enable` or a manager's
|
|
3788
|
+
* toggle, and never touched by a push (ADR 0011, amended by ADR 0013).
|
|
3789
|
+
*/
|
|
3790
|
+
enabled: boolean("enabled").notNull().default(false),
|
|
3912
3791
|
createdAt: createdAt(),
|
|
3913
3792
|
updatedAt: updatedAt()
|
|
3914
3793
|
}, (table) => [primaryKey({ columns: [table.orgId, table.key] })]);
|
|
3915
3794
|
const selectJourneySchema = createSelectSchema(journeys$1);
|
|
3916
3795
|
const insertJourneySchema = createInsertSchema(journeys$1);
|
|
3917
|
-
/**
|
|
3918
|
-
* Per-org, per-environment enable state for a journey key; the only
|
|
3919
|
-
* operator write on a journey, and the one gate on it. A row exists only
|
|
3920
|
-
* once someone turned the journey on or off in that environment: NO ROW
|
|
3921
|
-
* MEANS DISABLED (ADR 0011). A push writes none, so a key is off in both
|
|
3922
|
-
* environments until `cow enable` or a manager's toggle.
|
|
3923
|
-
*
|
|
3924
|
-
* Keyed by key rather than by a foreign key into `journeys` on purpose: the
|
|
3925
|
-
* flag outlives every push, so it survives a key that temporarily leaves a
|
|
3926
|
-
* project and comes back.
|
|
3927
|
-
*/
|
|
3928
|
-
const journeyStates = pgTable("journey_states", {
|
|
3929
|
-
orgId: text("org_id").notNull(),
|
|
3930
|
-
environment: environmentEnum("environment").notNull(),
|
|
3931
|
-
key: text("key").notNull(),
|
|
3932
|
-
enabled: boolean("enabled").notNull(),
|
|
3933
|
-
createdAt: createdAt(),
|
|
3934
|
-
updatedAt: updatedAt()
|
|
3935
|
-
}, (table) => [primaryKey({ columns: [
|
|
3936
|
-
table.orgId,
|
|
3937
|
-
table.environment,
|
|
3938
|
-
table.key
|
|
3939
|
-
] })]);
|
|
3940
3796
|
|
|
3941
3797
|
//#endregion
|
|
3942
3798
|
//#region ../../packages/db/src/schema/org-settings.ts
|
|
3943
3799
|
/**
|
|
3944
|
-
*
|
|
3945
|
-
*
|
|
3946
|
-
*
|
|
3947
|
-
*
|
|
3948
|
-
|
|
3800
|
+
* The ingestion policy for catalog governance. Single source of truth:
|
|
3801
|
+
* @cowliss/shared/governance derives INGESTION_POLICIES from
|
|
3802
|
+
* ingestionPolicyEnum.enumValues, so the enum and the settings contract
|
|
3803
|
+
* cannot drift.
|
|
3804
|
+
*/
|
|
3805
|
+
const ingestionPolicyEnum = pgEnum("ingestion_policy", ["permissive", "strict"]);
|
|
3806
|
+
/**
|
|
3807
|
+
* Per-org settings. A row exists only once something has been set: no row
|
|
3808
|
+
* means every default applies, which readers fill in (same convention as
|
|
3809
|
+
* journey_states). That keeps a fresh org zero-config and keeps the write
|
|
3810
|
+
* path from having to seed rows it does not own.
|
|
3949
3811
|
*
|
|
3950
|
-
*
|
|
3951
|
-
*
|
|
3952
|
-
*
|
|
3812
|
+
* Every value here is the organization's:
|
|
3813
|
+
* the ingestion policy, the event retention window, the sending kill
|
|
3814
|
+
* switch, the allowance override, and Clerk's cached profile.
|
|
3953
3815
|
*
|
|
3954
3816
|
* `slug` and `name` are the exception to "only what Cowliss owns": they are
|
|
3955
3817
|
* Clerk's, cached here by the API's org-sync middleware because the worker
|
|
@@ -3967,6 +3829,21 @@ const orgSettings = pgTable("org_settings", {
|
|
|
3967
3829
|
slug: text("slug"),
|
|
3968
3830
|
name: text("name"),
|
|
3969
3831
|
/**
|
|
3832
|
+
* Catalog governance: permissive accepts quarantined (unclassified) names
|
|
3833
|
+
* and observes them; strict routes them to the review queue instead of
|
|
3834
|
+
* applying them. One policy for the organization, applied to every write.
|
|
3835
|
+
*/
|
|
3836
|
+
ingestionPolicy: ingestionPolicyEnum("ingestion_policy").notNull().default("permissive"),
|
|
3837
|
+
/**
|
|
3838
|
+
* The ClickHouse TTL window for the org's events, in days. Null means the
|
|
3839
|
+
* default (18 months, EVENT_RETENTION_DAYS.production in
|
|
3840
|
+
* packages/shared); there is no "forever", and the API refuses a value
|
|
3841
|
+
* above that cap. Development events are trimmed at the shorter
|
|
3842
|
+
* development cap whatever this says: the rule is
|
|
3843
|
+
* `effectiveRetentionDays` in packages/shared.
|
|
3844
|
+
*/
|
|
3845
|
+
eventRetentionDays: integer("event_retention_days"),
|
|
3846
|
+
/**
|
|
3970
3847
|
* The operator-set monthly free allowance override, in micro-dollars,
|
|
3971
3848
|
* synced from the Clerk org's `publicMetadata.freeAllowanceMicros` by the
|
|
3972
3849
|
* same org-sync read that caches slug and name. Null (the default) means
|
|
@@ -4019,8 +3896,7 @@ const insertOrgSettingsSchema = createInsertSchema(orgSettings);
|
|
|
4019
3896
|
/**
|
|
4020
3897
|
* An org's cow project: the folder a developer runs `cow init` in, and the
|
|
4021
3898
|
* thing pushes belong to. An org may hold several, one per repo, each with
|
|
4022
|
-
* its own release sequence
|
|
4023
|
-
* own slice of the deployed journey rows. The name is how `cow.json` picks
|
|
3899
|
+
* its own release sequence and its own slice of the deployed journey rows. The name is how `cow.json` picks
|
|
4024
3900
|
* one, so it is unique within the org.
|
|
4025
3901
|
*/
|
|
4026
3902
|
const projects = pgTable("projects", {
|
|
@@ -4086,14 +3962,12 @@ const pushArtifacts = pgTable("push_artifacts", {
|
|
|
4086
3962
|
//#endregion
|
|
4087
3963
|
//#region ../../packages/db/src/schema/violations.ts
|
|
4088
3964
|
/**
|
|
4089
|
-
* Violations: one row per (orgId,
|
|
4090
|
-
*
|
|
4091
|
-
*
|
|
4092
|
-
*
|
|
4093
|
-
* per environment, so a name a dev box invented never shows up as a
|
|
4094
|
-
* production violation.
|
|
3965
|
+
* Violations: one row per (orgId, kind, name) observed in ingestion that
|
|
3966
|
+
* the tracking plan catalog does not define. Observe and flag, never
|
|
3967
|
+
* reject: the payload is always written, the violation only records the
|
|
3968
|
+
* governance gap.
|
|
4095
3969
|
*
|
|
4096
|
-
* Uniqueness on (orgId,
|
|
3970
|
+
* Uniqueness on (orgId, kind, name) dedupes repeat sightings:
|
|
4097
3971
|
* an unknown event fired a thousand times is one open violation whose
|
|
4098
3972
|
* lastSeenAt keeps bumping, not a thousand rows. The payload captures the first observed shape
|
|
4099
3973
|
* (inferred property types for events, inferred type for traits) so resolve
|
|
@@ -4111,7 +3985,6 @@ const violationStatusEnum = pgEnum("violation_status", [
|
|
|
4111
3985
|
const violations$1 = pgTable("violations", {
|
|
4112
3986
|
id: text("id").primaryKey(),
|
|
4113
3987
|
orgId: text("org_id").notNull(),
|
|
4114
|
-
environment: environmentEnum("environment").notNull(),
|
|
4115
3988
|
kind: violationKindEnum("kind").notNull(),
|
|
4116
3989
|
name: text("name").notNull(),
|
|
4117
3990
|
payload: jsonb("payload").$type().notNull(),
|
|
@@ -4136,7 +4009,7 @@ const violations$1 = pgTable("violations", {
|
|
|
4136
4009
|
mode: "date",
|
|
4137
4010
|
precision: 3
|
|
4138
4011
|
})
|
|
4139
|
-
}, (table) => [index("
|
|
4012
|
+
}, (table) => [index("violations_org_id_idx").on(table.orgId), uniqueIndex("violations_org_kind_name_unique").on(table.orgId, table.kind, table.name)]);
|
|
4140
4013
|
const selectViolationSchema = createSelectSchema(violations$1);
|
|
4141
4014
|
const insertViolationSchema = createInsertSchema(violations$1);
|
|
4142
4015
|
|
|
@@ -4145,7 +4018,6 @@ const insertViolationSchema = createInsertSchema(violations$1);
|
|
|
4145
4018
|
const quarantineEntries = pgTable("quarantine_entries", {
|
|
4146
4019
|
id: text("id").primaryKey(),
|
|
4147
4020
|
orgId: text("org_id").notNull(),
|
|
4148
|
-
environment: environmentEnum("environment").notNull(),
|
|
4149
4021
|
kind: violationKindEnum("kind").notNull(),
|
|
4150
4022
|
name: text("name").notNull(),
|
|
4151
4023
|
appId: text("app_id").notNull(),
|
|
@@ -4159,7 +4031,7 @@ const quarantineEntries = pgTable("quarantine_entries", {
|
|
|
4159
4031
|
}).notNull(),
|
|
4160
4032
|
createdAt: createdAt(),
|
|
4161
4033
|
updatedAt: updatedAt()
|
|
4162
|
-
}, (table) => [index("quarantine_entries_org_kind_name_idx").on(table.orgId, table.kind, table.name), index("
|
|
4034
|
+
}, (table) => [index("quarantine_entries_org_kind_name_idx").on(table.orgId, table.kind, table.name), index("quarantine_entries_org_created_idx").on(table.orgId, table.createdAt)]);
|
|
4163
4035
|
const selectQuarantineEntrySchema = createSelectSchema(quarantineEntries);
|
|
4164
4036
|
const insertQuarantineEntrySchema = createInsertSchema(quarantineEntries);
|
|
4165
4037
|
|
|
@@ -4170,10 +4042,6 @@ const insertQuarantineEntrySchema = createInsertSchema(quarantineEntries);
|
|
|
4170
4042
|
* never hand-maintained: the engine in packages/segments recomputes on every
|
|
4171
4043
|
* write and materializes the result into segment_members.
|
|
4172
4044
|
*
|
|
4173
|
-
* The definition is shared by the org's environments and declares which of
|
|
4174
|
-
* them it works on: `environments` defaults to both, and the recompute
|
|
4175
|
-
* evaluates a definition only for profiles in a declared environment.
|
|
4176
|
-
*
|
|
4177
4045
|
* Timestamps use millisecond precision, same rationale as apps: JS Dates
|
|
4178
4046
|
* carry ms only and cursor pagination compares createdAt for equality.
|
|
4179
4047
|
*/
|
|
@@ -4183,18 +4051,14 @@ const segments$1 = pgTable("segments", {
|
|
|
4183
4051
|
name: text("name").notNull(),
|
|
4184
4052
|
description: text("description"),
|
|
4185
4053
|
definition: jsonb("definition").$type().notNull(),
|
|
4186
|
-
/** The environments this definition works on; the enum is the check. */
|
|
4187
|
-
environments: environmentEnum("environments").array().notNull().default(["development", "production"]),
|
|
4188
4054
|
createdAt: createdAt(),
|
|
4189
4055
|
updatedAt: updatedAt()
|
|
4190
4056
|
}, (table) => [index("segments_org_id_idx").on(table.orgId), uniqueIndex("segments_org_id_name_unique").on(table.orgId, table.name)]);
|
|
4191
4057
|
/**
|
|
4192
|
-
* Materialized membership: one row per (segment,
|
|
4193
|
-
*
|
|
4194
|
-
*
|
|
4195
|
-
*
|
|
4196
|
-
* so the environment is part of the key: dropping an environment from a
|
|
4197
|
-
* definition drops that environment's rows and leaves the other's standing.
|
|
4058
|
+
* Materialized membership: one row per (segment, profile) currently in the
|
|
4059
|
+
* segment. Deleting a segment drops its members with it, and erasing a
|
|
4060
|
+
* profile drops its memberships, so neither can leave orphaned membership
|
|
4061
|
+
* behind.
|
|
4198
4062
|
*
|
|
4199
4063
|
* The (orgId, profileId) index is the recompute lookup: every write asks
|
|
4200
4064
|
* "which of this org's segments does this profile currently belong to?"
|
|
@@ -4203,19 +4067,13 @@ const segments$1 = pgTable("segments", {
|
|
|
4203
4067
|
const segmentMembers = pgTable("segment_members", {
|
|
4204
4068
|
segmentId: text("segment_id").notNull().references(() => segments$1.id, { onDelete: "cascade" }),
|
|
4205
4069
|
orgId: text("org_id").notNull(),
|
|
4206
|
-
/** The member profile's environment; always the profile's own. */
|
|
4207
|
-
environment: environmentEnum("environment").notNull(),
|
|
4208
4070
|
profileId: text("profile_id").notNull().references(() => profiles.id, { onDelete: "cascade" }),
|
|
4209
4071
|
enteredAt: timestamp("entered_at", {
|
|
4210
4072
|
withTimezone: true,
|
|
4211
4073
|
mode: "date",
|
|
4212
4074
|
precision: 3
|
|
4213
4075
|
}).notNull().defaultNow()
|
|
4214
|
-
}, (table) => [primaryKey({ columns: [
|
|
4215
|
-
table.segmentId,
|
|
4216
|
-
table.environment,
|
|
4217
|
-
table.profileId
|
|
4218
|
-
] }), index("segment_members_org_id_profile_id_idx").on(table.orgId, table.profileId)]);
|
|
4076
|
+
}, (table) => [primaryKey({ columns: [table.segmentId, table.profileId] }), index("segment_members_org_id_profile_id_idx").on(table.orgId, table.profileId)]);
|
|
4219
4077
|
const selectSegmentSchema = createSelectSchema(segments$1);
|
|
4220
4078
|
const insertSegmentSchema = createInsertSchema(segments$1);
|
|
4221
4079
|
|
|
@@ -4287,14 +4145,14 @@ const insertSenderDomainSchema = createInsertSchema(senderDomains);
|
|
|
4287
4145
|
* source is a way data reaches it, so "App A via Clerk" and "App A via the
|
|
4288
4146
|
* SDK" land on the same app and therefore on the same profiles.
|
|
4289
4147
|
*
|
|
4290
|
-
* Every app has
|
|
4291
|
-
*
|
|
4292
|
-
*
|
|
4293
|
-
* beside it.
|
|
4148
|
+
* Every app has one source of kind `api`, the first-party SDK and HTTP
|
|
4149
|
+
* path, created with the app. It has an empty config and no credential, and
|
|
4150
|
+
* archiving it is that app's ingestion kill switch. Provider kinds sit
|
|
4151
|
+
* beside it, singleton per app.
|
|
4294
4152
|
*
|
|
4295
4153
|
* `config` holds the per-kind source configuration and `signingSecret` the
|
|
4296
4154
|
* credential of the one kind that has one today (Clerk's webhook signing
|
|
4297
|
-
* secret), stored write-only (the
|
|
4155
|
+
* secret), stored write-only (the webhooks show-once pattern: never on a
|
|
4298
4156
|
* DTO). `kind` is immutable after creation; the webhook route dispatches on
|
|
4299
4157
|
* it. `lastReceivedAt` is stamped per accepted delivery so the dashboard can
|
|
4300
4158
|
* tell a wired-up source from a silent one.
|
|
@@ -4529,20 +4387,44 @@ const selectWalletTopupSchema = createSelectSchema(walletTopups);
|
|
|
4529
4387
|
const insertWalletTopupSchema = createInsertSchema(walletTopups);
|
|
4530
4388
|
|
|
4531
4389
|
//#endregion
|
|
4532
|
-
//#region ../../packages/
|
|
4390
|
+
//#region ../../packages/db/src/schema/webhooks.ts
|
|
4533
4391
|
/**
|
|
4534
|
-
*
|
|
4535
|
-
*
|
|
4536
|
-
* `
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
*
|
|
4541
|
-
*
|
|
4542
|
-
*
|
|
4543
|
-
*
|
|
4392
|
+
* Webhooks: named URLs that journeys POST to by name. A webhook is the
|
|
4393
|
+
* organization's: the name is unique per org, and a journey step naming
|
|
4394
|
+
* `webhook("crm")` resolves to the same row for every execution. A developer
|
|
4395
|
+
* who wants a local receiver creates a second webhook under another name.
|
|
4396
|
+
* Ms-precision timestamps, as everywhere, so a cursor round-trips.
|
|
4397
|
+
*
|
|
4398
|
+
* WEBHOOK_PAYLOAD_VERSION (packages/shared) pins the webhook payload shape;
|
|
4399
|
+
* the column records the version each webhook was created against.
|
|
4400
|
+
* signingSecret is the Standard Webhooks signing secret, generated at
|
|
4401
|
+
* creation and shown once — the API DTO omits it after the create response.
|
|
4544
4402
|
*/
|
|
4545
|
-
const
|
|
4403
|
+
const webhooks$1 = pgTable("webhooks", {
|
|
4404
|
+
id: text("id").primaryKey(),
|
|
4405
|
+
orgId: text("org_id").notNull(),
|
|
4406
|
+
name: text("name").notNull(),
|
|
4407
|
+
config: jsonb("config").$type().notNull(),
|
|
4408
|
+
webhookPayloadVersion: integer("webhook_payload_version").notNull().default(1),
|
|
4409
|
+
signingSecret: text("signing_secret"),
|
|
4410
|
+
/**
|
|
4411
|
+
* Auto-disable state, mirroring how receivers treat us: the webhook send
|
|
4412
|
+
* activity bumps the counter once per failed delivery attempt (not once
|
|
4413
|
+
* per Temporal retry) and stamps disabledAt at the threshold. A disabled
|
|
4414
|
+
* webhook records `skipped_disabled` until an admin clears it; a
|
|
4415
|
+
* successful delivery resets the counter.
|
|
4416
|
+
*/
|
|
4417
|
+
consecutiveFailures: integer("consecutive_failures").notNull().default(0),
|
|
4418
|
+
disabledAt: timestamp("disabled_at", {
|
|
4419
|
+
withTimezone: true,
|
|
4420
|
+
mode: "date",
|
|
4421
|
+
precision: 3
|
|
4422
|
+
}),
|
|
4423
|
+
createdAt: createdAt(),
|
|
4424
|
+
updatedAt: updatedAt()
|
|
4425
|
+
}, (table) => [index("webhooks_org_id_idx").on(table.orgId), uniqueIndex("webhooks_org_id_name_unique").on(table.orgId, table.name)]);
|
|
4426
|
+
const selectWebhookSchema = createSelectSchema(webhooks$1);
|
|
4427
|
+
const insertWebhookSchema = createInsertSchema(webhooks$1);
|
|
4546
4428
|
|
|
4547
4429
|
//#endregion
|
|
4548
4430
|
//#region ../../packages/shared/src/apps.ts
|
|
@@ -4553,10 +4435,10 @@ const environmentsSchema = z.array(environmentSchema).min(1, "at least one envir
|
|
|
4553
4435
|
* JSON.stringify already emits it for Date values.
|
|
4554
4436
|
*
|
|
4555
4437
|
* An app is the attribution unit and nothing else: it carries no kind, no
|
|
4556
|
-
* config
|
|
4557
|
-
*
|
|
4558
|
-
*
|
|
4559
|
-
*
|
|
4438
|
+
* config and no credential (its sources carry those, see
|
|
4439
|
+
* sources.ts). The two extra fields are read-model facts about those
|
|
4440
|
+
* sources, which is what the dashboard's list needs to say whether an app
|
|
4441
|
+
* has a pipe wired up without a request per card.
|
|
4560
4442
|
*/
|
|
4561
4443
|
const appSchema = selectAppSchema.extend({
|
|
4562
4444
|
createdAt: z.iso.datetime(),
|
|
@@ -4568,11 +4450,7 @@ const appSchema = selectAppSchema.extend({
|
|
|
4568
4450
|
lastReceivedAt: z.iso.datetime().nullable()
|
|
4569
4451
|
});
|
|
4570
4452
|
const nameSchema$2 = z.string().trim().min(1, "name is required").max(100, "name must be at most 100 characters");
|
|
4571
|
-
const createAppBodySchema = z.object({ data: z.object({
|
|
4572
|
-
name: nameSchema$2,
|
|
4573
|
-
/** Immutable after creation; production when omitted. */
|
|
4574
|
-
environment: environmentSchema.default("production")
|
|
4575
|
-
}) });
|
|
4453
|
+
const createAppBodySchema = z.object({ data: z.object({ name: nameSchema$2 }) });
|
|
4576
4454
|
const updateAppBodySchema = z.object({ data: z.object({
|
|
4577
4455
|
name: nameSchema$2.optional(),
|
|
4578
4456
|
status: z.literal("archived").optional()
|
|
@@ -4769,15 +4647,44 @@ const triggerSchema = z.union([z.strictObject({
|
|
|
4769
4647
|
appId: patternSchema.optional()
|
|
4770
4648
|
}), z.strictObject({ segment: z.string().min(1) })]);
|
|
4771
4649
|
/**
|
|
4772
|
-
*
|
|
4773
|
-
*
|
|
4774
|
-
*
|
|
4775
|
-
*
|
|
4776
|
-
*
|
|
4777
|
-
|
|
4778
|
-
|
|
4650
|
+
* The address half of a from-header: a local part, an `@`, and a dotted
|
|
4651
|
+
* domain. Deliberately narrower than RFC 5322 (no quoted local parts, no
|
|
4652
|
+
* address literals): every character it refuses would have to be quoted or
|
|
4653
|
+
* escaped to survive a header, and none of them belong in an address a
|
|
4654
|
+
* journey sends from.
|
|
4655
|
+
*/
|
|
4656
|
+
const FROM_ADDRESS = /^[^\s@<>",;]+@([^\s@<>",;.]+(?:\.[^\s@<>",;.]+)+)$/;
|
|
4657
|
+
/**
|
|
4658
|
+
* Parse a journey's or a send's `from`: `addr@domain`, or
|
|
4659
|
+
* `Name <addr@domain>` with the name optionally quoted. Null when it is
|
|
4660
|
+
* neither, which is what `cow build` refuses on and what the send-time
|
|
4661
|
+
* domain gate turns into a skip.
|
|
4662
|
+
*
|
|
4663
|
+
* One parser, and the send facade reuses it. A control character anywhere is
|
|
4664
|
+
* a refusal rather than something to strip: it survives quoting and would
|
|
4665
|
+
* inject a second header.
|
|
4666
|
+
*/
|
|
4667
|
+
function parseFromAddress(from) {
|
|
4668
|
+
const text = from.trim();
|
|
4669
|
+
if (/\p{Cc}/u.test(text)) return null;
|
|
4670
|
+
const angled = /^(.*?)\s*<([^<>]*)>$/.exec(text);
|
|
4671
|
+
const address = (angled?.[2] ?? text).trim();
|
|
4672
|
+
const domain = FROM_ADDRESS.exec(address)?.[1];
|
|
4673
|
+
if (!domain) return null;
|
|
4674
|
+
const name = (angled?.[1] ?? "").trim().replace(/^"(.*)"$/s, "$1").replace(/\\(.)/g, "$1").trim();
|
|
4675
|
+
return {
|
|
4676
|
+
...name ? { name } : {},
|
|
4677
|
+
address,
|
|
4678
|
+
domain: domain.toLowerCase()
|
|
4679
|
+
};
|
|
4680
|
+
}
|
|
4681
|
+
/**
|
|
4682
|
+
* The address a journey or one send leaves as: a string, not the name of a
|
|
4683
|
+
* configured row (ADR 0014). The domain is checked against the
|
|
4684
|
+
* organization's verified ones at push time and again at send time; the
|
|
4685
|
+
* shape is all that is checked here, because it is all a build can know.
|
|
4779
4686
|
*/
|
|
4780
|
-
const
|
|
4687
|
+
const fromSchema = z.string().trim().max(200, "from must be at most 200 characters").refine((value) => parseFromAddress(value) !== null, { message: "from must be an address (\"billing@acme.com\") or a name and address (\"Billing <billing@acme.com>\")" });
|
|
4781
4688
|
/** A content address: `sha256:` plus the lowercase hex digest. */
|
|
4782
4689
|
const digestSchema = z.string().regex(/^sha256:[0-9a-f]{64}$/, "digest must be sha256:<64 hex>");
|
|
4783
4690
|
/**
|
|
@@ -4813,13 +4720,6 @@ const SPINE_ENTRY_NAMES = [
|
|
|
4813
4720
|
const spineEntrySchema = z.object({
|
|
4814
4721
|
name: z.enum(SPINE_ENTRY_NAMES),
|
|
4815
4722
|
detail: z.string().max(200).optional(),
|
|
4816
|
-
/**
|
|
4817
|
-
* The sender identity a `send.email` call named for itself, overriding
|
|
4818
|
-
* the journey's own. Present only when the author wrote one on the call,
|
|
4819
|
-
* which is what lets the deploy warning and the journey detail page name
|
|
4820
|
-
* the override without re-reading the code.
|
|
4821
|
-
*/
|
|
4822
|
-
senderIdentity: z.string().max(100).optional(),
|
|
4823
4723
|
/** A `waitForEvent` timeout, as the author wrote it. */
|
|
4824
4724
|
timeout: z.string().max(50).optional(),
|
|
4825
4725
|
get steps() {
|
|
@@ -4831,10 +4731,10 @@ const spineEntrySchema = z.object({
|
|
|
4831
4731
|
}).meta({ id: "JourneySpineEntry" });
|
|
4832
4732
|
/**
|
|
4833
4733
|
* One journey in a stored manifest. A plain (non-strict) object on purpose:
|
|
4834
|
-
* a manifest pushed before the author's rollout gate went away still
|
|
4835
|
-
*
|
|
4836
|
-
* (ADR 0011). Zod strips
|
|
4837
|
-
*
|
|
4734
|
+
* a manifest pushed before the author's rollout gate went away still
|
|
4735
|
+
* carries keys that no longer exist, and every stored manifest has to keep
|
|
4736
|
+
* parsing for good (ADR 0011). Zod strips them, and the journey runs when
|
|
4737
|
+
* its `enabled` flag says so, which is the one gate there is.
|
|
4838
4738
|
*/
|
|
4839
4739
|
const manifestJourneySchema = z.object({
|
|
4840
4740
|
key: journeyKeySchema,
|
|
@@ -4848,16 +4748,12 @@ const manifestJourneySchema = z.object({
|
|
|
4848
4748
|
*/
|
|
4849
4749
|
purpose: consentPurposeKeySchema,
|
|
4850
4750
|
/**
|
|
4851
|
-
* The
|
|
4852
|
-
*
|
|
4853
|
-
*
|
|
4854
|
-
*
|
|
4855
|
-
*
|
|
4856
|
-
* Optional here and required at `defineJourney`, exactly like `purposes`: a
|
|
4857
|
-
* version pushed before the field existed carries none and its stored
|
|
4858
|
-
* manifest still parses. The author's build is where the error is useful.
|
|
4751
|
+
* The address every `send.email` in this journey goes out as, unless the
|
|
4752
|
+
* call names its own. Optional everywhere: an omitted one is the
|
|
4753
|
+
* organization's shared fallback address, which is what makes day-one
|
|
4754
|
+
* sending zero-config (ADR 0014).
|
|
4859
4755
|
*/
|
|
4860
|
-
|
|
4756
|
+
from: fromSchema.optional(),
|
|
4861
4757
|
spine: z.array(spineEntrySchema),
|
|
4862
4758
|
bundle: digestSchema
|
|
4863
4759
|
});
|
|
@@ -4934,7 +4830,7 @@ const versionManifestSchema = z.union([manifestJourneySchema, manifestTemplateSc
|
|
|
4934
4830
|
* further in the future than the clock-skew tolerance (the spec's "future
|
|
4935
4831
|
* timestamps are rejected with 422" rule). Absent stays valid. Used by the
|
|
4936
4832
|
* track and identify payload schemas; older-than-retention is a service
|
|
4937
|
-
* check (withinRetention) because the window is
|
|
4833
|
+
* check (withinRetention) because the window is the org's setting.
|
|
4938
4834
|
*/
|
|
4939
4835
|
function notFutureTimestamp(value) {
|
|
4940
4836
|
if (value === void 0) return true;
|
|
@@ -5015,7 +4911,6 @@ const identifyBodySchema = z.object({ data: identifyDataSchema });
|
|
|
5015
4911
|
const eventDtoSchema = z.object({
|
|
5016
4912
|
id: z.string(),
|
|
5017
4913
|
orgId: z.string(),
|
|
5018
|
-
environment: environmentSchema,
|
|
5019
4914
|
/** The app the event is attributed to: derived from the source. */
|
|
5020
4915
|
appId: z.string(),
|
|
5021
4916
|
/** The pipe it arrived through: what the caller named on the wire. */
|
|
@@ -5052,11 +4947,10 @@ const trackDataSchema = trackFieldsSchema.refine((data) => notFutureTimestamp(da
|
|
|
5052
4947
|
});
|
|
5053
4948
|
const trackBodySchema = z.object({ data: trackDataSchema });
|
|
5054
4949
|
/**
|
|
5055
|
-
*
|
|
4950
|
+
* The org-wide event feed query (GET /v1/events). Same opaque cursor as
|
|
5056
4951
|
* every other list; the sort key is the event timestamp, newest first, so
|
|
5057
4952
|
* the cursor's sortAt slot carries an event timestamp here. appId, sourceId
|
|
5058
|
-
* and event are exact-match filters, all optional.
|
|
5059
|
-
* from the selection header, never from the query.
|
|
4953
|
+
* and event are exact-match filters, all optional.
|
|
5060
4954
|
*
|
|
5061
4955
|
* This is the one place a pipe id is a filter: unlike a stored segment
|
|
5062
4956
|
* definition, a query param is read-time state, so a stale `?sourceId=` is a
|
|
@@ -5076,8 +4970,8 @@ const listEventsQuerySchema = paginationQuerySchema.extend({
|
|
|
5076
4970
|
profileId: z.string().trim().min(1).max(200).optional()
|
|
5077
4971
|
});
|
|
5078
4972
|
/**
|
|
5079
|
-
* Event stats (GET /v1/events/stats)
|
|
5080
|
-
*
|
|
4973
|
+
* Event stats (GET /v1/events/stats): the overview page's volume chart and
|
|
4974
|
+
* per-app breakdown. `volume` is
|
|
5081
4975
|
* zero-filled per UTC day over the whole `days` window ending today, so the
|
|
5082
4976
|
* chart gets a continuous time axis; `byApp` is sorted busiest-first with
|
|
5083
4977
|
* app names joined in.
|
|
@@ -5097,8 +4991,8 @@ const eventStatsDtoSchema = z.object({
|
|
|
5097
4991
|
}))
|
|
5098
4992
|
});
|
|
5099
4993
|
/**
|
|
5100
|
-
* Per-name activity (GET /v1/events/stats/names)
|
|
5101
|
-
*
|
|
4994
|
+
* Per-name activity (GET /v1/events/stats/names): what the event catalog
|
|
4995
|
+
* shows beside each entry. `count` is
|
|
5102
4996
|
* windowed over `days`; `lastSeenAt` is not windowed and is null only for a
|
|
5103
4997
|
* name that has never been seen, so "quiet for two months" reads as a zero
|
|
5104
4998
|
* count next to a real date rather than as no history.
|
|
@@ -5124,7 +5018,7 @@ const eventNameStatsDtoSchema = z.object({
|
|
|
5124
5018
|
* API in one request instead of thousands of round trips.
|
|
5125
5019
|
*
|
|
5126
5020
|
* A batch names one source for every item, `{ sourceId, items }`, so it
|
|
5127
|
-
* never spans apps
|
|
5021
|
+
* never spans apps. Each item is one
|
|
5128
5022
|
* flattened identify or track call tagged by `type`; the field schemas are
|
|
5129
5023
|
* the single-call ones minus `sourceId`, so an item valid here is valid
|
|
5130
5024
|
* there. Batch writes are QUIET in one sense: they skip raw-event journey
|
|
@@ -5427,8 +5321,11 @@ const listConsentPurposesQuerySchema = paginationQuerySchema;
|
|
|
5427
5321
|
*/
|
|
5428
5322
|
const DELIVERY_CHANNELS = deliveryChannelEnum.enumValues;
|
|
5429
5323
|
const DELIVERY_STATUSES = deliveryStatusEnum.enumValues;
|
|
5324
|
+
/** Why a send happened: a journey ran, a member tested, or a caller sent. */
|
|
5325
|
+
const DELIVERY_KINDS = deliveryKindEnum.enumValues;
|
|
5430
5326
|
const deliveryStatusSchema = z.enum(DELIVERY_STATUSES);
|
|
5431
5327
|
const deliveryChannelSchema = z.enum(DELIVERY_CHANNELS);
|
|
5328
|
+
const deliveryKindSchema = z.enum(DELIVERY_KINDS);
|
|
5432
5329
|
/**
|
|
5433
5330
|
* The SNS HTTPS envelope, common to every message SNS POSTs to the feedback
|
|
5434
5331
|
* endpoint. `Message` is a JSON STRING (the SES event for a `Notification`,
|
|
@@ -5528,18 +5425,45 @@ const deliverySchema = selectDeliverySchema.omit({
|
|
|
5528
5425
|
settledAt: z.iso.datetime().nullable()
|
|
5529
5426
|
});
|
|
5530
5427
|
/**
|
|
5531
|
-
* The message a dry run
|
|
5532
|
-
*
|
|
5533
|
-
* rendering: SES got it, and the log does not keep a second
|
|
5428
|
+
* The message a dry run rendered, stored under `payload.rendered` beside the
|
|
5429
|
+
* template data, so a developer can read what would have gone out. A real
|
|
5430
|
+
* send carries no rendering: SES got it, and the log does not keep a second
|
|
5431
|
+
* copy.
|
|
5534
5432
|
*/
|
|
5535
5433
|
const renderedEmailSchema = z.object({
|
|
5536
5434
|
subject: z.string(),
|
|
5537
5435
|
html: z.string(),
|
|
5538
|
-
text: z.string()
|
|
5436
|
+
text: z.string(),
|
|
5437
|
+
/**
|
|
5438
|
+
* Where a reply would have gone, when the send named one. Absent on a
|
|
5439
|
+
* test send and wherever none was named, which is replies going to the
|
|
5440
|
+
* from-address.
|
|
5441
|
+
*/
|
|
5442
|
+
replyTo: z.email().optional()
|
|
5539
5443
|
});
|
|
5540
5444
|
/** One delivery with the payload the send rendered (GET /v1/deliveries/:id). */
|
|
5541
5445
|
const deliveryDetailSchema = deliverySchema.extend({ payload: z.object({ rendered: renderedEmailSchema.optional() }).catchall(z.unknown()) });
|
|
5542
5446
|
/**
|
|
5447
|
+
* The `payload` a delivery of kind `api` carries: the envelope the caller of
|
|
5448
|
+
* the send facade wrote, and never the bodies (ADR 0014). Every field is
|
|
5449
|
+
* optional because the caller named only some of them, and the row is read
|
|
5450
|
+
* back through this rather than cast, so a row written by an older shape
|
|
5451
|
+
* reads as a message with fields missing instead of one with a type nobody
|
|
5452
|
+
* checked.
|
|
5453
|
+
*/
|
|
5454
|
+
const apiDeliveryPayloadSchema = z.object({
|
|
5455
|
+
from: z.string().optional(),
|
|
5456
|
+
to: z.array(z.string()).optional(),
|
|
5457
|
+
cc: z.array(z.string()).optional(),
|
|
5458
|
+
bcc: z.array(z.string()).optional(),
|
|
5459
|
+
reply_to: z.array(z.string()).optional(),
|
|
5460
|
+
subject: z.string().optional(),
|
|
5461
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
5462
|
+
/** The template key that wrote the message, and the props it took. */
|
|
5463
|
+
template: z.string().optional(),
|
|
5464
|
+
variables: z.record(z.string(), z.unknown()).optional()
|
|
5465
|
+
});
|
|
5466
|
+
/**
|
|
5543
5467
|
* List query for the deliveries log. Every filter is exact-match and
|
|
5544
5468
|
* optional; combining them ANDs them.
|
|
5545
5469
|
*
|
|
@@ -5553,113 +5477,29 @@ const listDeliveriesQuerySchema = paginationQuerySchema.extend({
|
|
|
5553
5477
|
direction: sortDirectionSchema.default("desc"),
|
|
5554
5478
|
status: deliveryStatusSchema.optional(),
|
|
5555
5479
|
journey: z.string().trim().min(1).max(200).optional(),
|
|
5556
|
-
/** The template or
|
|
5480
|
+
/** The template or webhook this attempt was for; a template page's own log. */
|
|
5557
5481
|
step: z.string().trim().min(1).max(200).optional(),
|
|
5558
5482
|
channel: deliveryChannelSchema.optional(),
|
|
5483
|
+
/** Why the send happened: a journey, a test send, or a direct API send. */
|
|
5484
|
+
kind: deliveryKindSchema.optional(),
|
|
5559
5485
|
appId: z.string().trim().min(1).max(200).optional(),
|
|
5560
5486
|
/** One person's deliveries, merged ids included, same as the event feed. */
|
|
5561
5487
|
profileId: z.string().trim().min(1).max(200).optional(),
|
|
5562
5488
|
/**
|
|
5563
5489
|
* Only deliveries touched at or after this instant, by `updatedAt`: the
|
|
5564
|
-
* cursor a tail (`cow dev`) polls with, so a
|
|
5565
|
-
*
|
|
5566
|
-
*
|
|
5567
|
-
*
|
|
5490
|
+
* cursor a tail (`cow dev`) polls with, so a send and its later feedback
|
|
5491
|
+
* transition both arrive. Inclusive for the same reason as the execution
|
|
5492
|
+
* filter, and paged the same way: `createdAt` still orders the page, this
|
|
5493
|
+
* only narrows it.
|
|
5568
5494
|
*/
|
|
5569
5495
|
since: z.iso.datetime().optional()
|
|
5570
5496
|
});
|
|
5571
5497
|
|
|
5572
|
-
//#endregion
|
|
5573
|
-
//#region ../../packages/shared/src/destinations.ts
|
|
5574
|
-
/**
|
|
5575
|
-
* Destinations API contracts. Derived from the Drizzle table via drizzle-zod.
|
|
5576
|
-
* signingSecret is deliberately absent from the DTO: it is shown exactly
|
|
5577
|
-
* once, in the create response (destinationCreatedSchema). consecutiveFailures
|
|
5578
|
-
* is absent too: it is the auto-disable counter's internal state, and
|
|
5579
|
-
* disabledAt is the part the dashboard banner needs.
|
|
5580
|
-
*
|
|
5581
|
-
* A destination belongs to one environment, chosen at creation and
|
|
5582
|
-
* immutable like a source's, and names are unique per org and environment,
|
|
5583
|
-
* so the same name points at a different receiver in each.
|
|
5584
|
-
*/
|
|
5585
|
-
const destinationSchema = selectDestinationSchema.extend({
|
|
5586
|
-
createdAt: z.iso.datetime(),
|
|
5587
|
-
updatedAt: z.iso.datetime(),
|
|
5588
|
-
disabledAt: z.iso.datetime().nullable()
|
|
5589
|
-
}).omit({
|
|
5590
|
-
signingSecret: true,
|
|
5591
|
-
consecutiveFailures: true
|
|
5592
|
-
});
|
|
5593
|
-
/** The two kinds of destination, from the table's own enum. */
|
|
5594
|
-
const destinationTypeSchema = destinationSchema.shape.type;
|
|
5595
|
-
const destinationCreatedSchema = destinationSchema.extend({ signingSecret: z.string().optional() });
|
|
5596
|
-
const webhookConfigSchema = z.object({ url: z.url("config.url must be a valid URL").refine((url) => {
|
|
5597
|
-
try {
|
|
5598
|
-
return ["http:", "https:"].includes(new URL(url).protocol);
|
|
5599
|
-
} catch {
|
|
5600
|
-
return false;
|
|
5601
|
-
}
|
|
5602
|
-
}, { message: "config.url must use http or https" }) });
|
|
5603
|
-
/**
|
|
5604
|
-
* The address shape only. WHICH domain is allowed is not a static fact: it
|
|
5605
|
-
* is the shared fallback domain or one this org has verified, which needs a
|
|
5606
|
-
* database read, so the rule lives in the destinations service (write time)
|
|
5607
|
-
* and the send gate (send time) instead of here.
|
|
5608
|
-
*/
|
|
5609
|
-
const senderIdentityConfigSchema = z.object({
|
|
5610
|
-
fromName: z.string().trim().min(1, "config.fromName is required").max(100).refine((name) => !/\p{Cc}/u.test(name), { message: "config.fromName must not contain control characters" }),
|
|
5611
|
-
fromEmail: z.email("config.fromEmail must be a valid email address")
|
|
5612
|
-
});
|
|
5613
|
-
/**
|
|
5614
|
-
* The two destination shapes, named individually so the dashboard's create
|
|
5615
|
-
* and edit forms can resolve against exactly one of them. A form knows its
|
|
5616
|
-
* type before the user types anything, and zodResolver over a member is
|
|
5617
|
-
* what gives react-hook-form a non-union field path to register.
|
|
5618
|
-
*/
|
|
5619
|
-
const webhookDestinationInputSchema = z.object({
|
|
5620
|
-
name: destinationNameSchema,
|
|
5621
|
-
/** The environment this destination lives in; immutable after creation. */
|
|
5622
|
-
environment: environmentSchema,
|
|
5623
|
-
type: z.literal("webhook"),
|
|
5624
|
-
config: webhookConfigSchema
|
|
5625
|
-
});
|
|
5626
|
-
const senderIdentityDestinationInputSchema = z.object({
|
|
5627
|
-
name: destinationNameSchema,
|
|
5628
|
-
/** The environment this destination lives in; immutable after creation. */
|
|
5629
|
-
environment: environmentSchema,
|
|
5630
|
-
type: z.literal("sender_identity"),
|
|
5631
|
-
config: senderIdentityConfigSchema
|
|
5632
|
-
});
|
|
5633
|
-
const createDestinationBodySchema = z.object({ data: z.discriminatedUnion("type", [webhookDestinationInputSchema, senderIdentityDestinationInputSchema]) });
|
|
5634
|
-
/**
|
|
5635
|
-
* Destination update. `enabled` is the admin's half of the auto-disable
|
|
5636
|
-
* loop: the webhook send activity stamps `disabledAt` after enough
|
|
5637
|
-
* consecutive failures and every send after that records `skipped_disabled`,
|
|
5638
|
-
* so without a way to clear it the only exit would be deleting the
|
|
5639
|
-
* destination, which throws away the signing secret the receiver is
|
|
5640
|
-
* configured with. `enabled: true` clears the stamp and the failure counter;
|
|
5641
|
-
* `enabled: false` is the same switch operated by hand.
|
|
5642
|
-
*/
|
|
5643
|
-
const updateDestinationBodySchema = z.object({ data: z.object({
|
|
5644
|
-
name: destinationNameSchema.optional(),
|
|
5645
|
-
config: z.unknown().optional(),
|
|
5646
|
-
enabled: z.boolean().optional()
|
|
5647
|
-
}).refine((data) => data.name !== void 0 || data.config !== void 0 || data.enabled !== void 0, { message: "at least one field (name, config, or enabled) is required" }) });
|
|
5648
|
-
/**
|
|
5649
|
-
* `q` is a substring search over the destination's name; `type` narrows to
|
|
5650
|
-
* one kind, because the webhooks page and the sender identities section on
|
|
5651
|
-
* the domains page each want one and the table holds both.
|
|
5652
|
-
*/
|
|
5653
|
-
const listDestinationsQuerySchema = paginationQuerySchema.extend({
|
|
5654
|
-
q: searchQuerySchema,
|
|
5655
|
-
type: destinationTypeSchema.optional()
|
|
5656
|
-
});
|
|
5657
|
-
|
|
5658
5498
|
//#endregion
|
|
5659
5499
|
//#region ../../packages/shared/src/domains.ts
|
|
5660
5500
|
/**
|
|
5661
5501
|
* Sending-domain contracts. Derived from the Drizzle table so the row and
|
|
5662
|
-
* the DTO cannot drift, same as deliveries,
|
|
5502
|
+
* the DTO cannot drift, same as deliveries, webhooks, and suppressions.
|
|
5663
5503
|
*
|
|
5664
5504
|
* Verification itself is SES's; everything here describes the claim and the
|
|
5665
5505
|
* mirrored answer.
|
|
@@ -5715,20 +5555,6 @@ const domainDnsSetupSchema = z.object({
|
|
|
5715
5555
|
provider: z.string().nullable()
|
|
5716
5556
|
});
|
|
5717
5557
|
|
|
5718
|
-
//#endregion
|
|
5719
|
-
//#region ../../packages/shared/src/enabled.ts
|
|
5720
|
-
/**
|
|
5721
|
-
* The enable flag per environment; absent rows read as off (ADR 0011).
|
|
5722
|
-
*
|
|
5723
|
-
* Its own module because both `journeys.ts` and `pushes.ts` need it and
|
|
5724
|
-
* neither may import the other: a journey carries the flag, and so does the
|
|
5725
|
-
* project status a push is read back through.
|
|
5726
|
-
*/
|
|
5727
|
-
const journeyEnabledSchema = z.object({
|
|
5728
|
-
development: z.boolean(),
|
|
5729
|
-
production: z.boolean()
|
|
5730
|
-
});
|
|
5731
|
-
|
|
5732
5558
|
//#endregion
|
|
5733
5559
|
//#region ../../packages/shared/src/patterns.ts
|
|
5734
5560
|
/**
|
|
@@ -5856,8 +5682,8 @@ const properties = z.record(z.string(), z.unknown());
|
|
|
5856
5682
|
* them. A name outside the union is rejected, never dispatched.
|
|
5857
5683
|
*
|
|
5858
5684
|
* Every `args` is strict, and that is the tenancy boundary made mechanical:
|
|
5859
|
-
* a module that returns an `orgId
|
|
5860
|
-
*
|
|
5685
|
+
* a module that returns an `orgId` or any other field beside the ones a
|
|
5686
|
+
* capability takes fails the parse instead of having it
|
|
5861
5687
|
* quietly dropped. Tenant context comes from the workflow input, never from
|
|
5862
5688
|
* guest output, and this is where saying so becomes checkable.
|
|
5863
5689
|
*/
|
|
@@ -5879,12 +5705,14 @@ const commandSchema = z.discriminatedUnion("name", [
|
|
|
5879
5705
|
template: journeyKeySchema,
|
|
5880
5706
|
props: properties,
|
|
5881
5707
|
/**
|
|
5882
|
-
*
|
|
5883
|
-
*
|
|
5884
|
-
* one
|
|
5885
|
-
*
|
|
5708
|
+
* The address this mail leaves as. The guest SDK fills in the
|
|
5709
|
+
* journey's own whenever the call does not name one, so the host has
|
|
5710
|
+
* one resolution path and never reads the manifest to find it;
|
|
5711
|
+
* absent on both is the organization's shared fallback address.
|
|
5886
5712
|
*/
|
|
5887
|
-
|
|
5713
|
+
from: fromSchema.optional(),
|
|
5714
|
+
/** Where a reply to this one mail goes, instead of the from-address. */
|
|
5715
|
+
replyTo: z.email().optional()
|
|
5888
5716
|
})
|
|
5889
5717
|
}),
|
|
5890
5718
|
z.object({
|
|
@@ -5965,7 +5793,7 @@ const executionLimitsSchema = z.object({
|
|
|
5965
5793
|
logLineBytes: z.number().int().positive()
|
|
5966
5794
|
});
|
|
5967
5795
|
const journeyStepInputSchema = z.object({
|
|
5968
|
-
protocol: z.literal(
|
|
5796
|
+
protocol: z.literal(3),
|
|
5969
5797
|
kind: z.literal("journey"),
|
|
5970
5798
|
key: journeyKeySchema,
|
|
5971
5799
|
event: guestEventSchema,
|
|
@@ -6005,7 +5833,7 @@ const journeyStepOutputSchema = z.discriminatedUnion("status", [
|
|
|
6005
5833
|
})
|
|
6006
5834
|
]);
|
|
6007
5835
|
const templateRenderInputSchema = z.object({
|
|
6008
|
-
protocol: z.literal(
|
|
5836
|
+
protocol: z.literal(3),
|
|
6009
5837
|
kind: z.literal("template"),
|
|
6010
5838
|
key: journeyKeySchema,
|
|
6011
5839
|
props: properties
|
|
@@ -6024,7 +5852,7 @@ const manifestInputSchema = z.object({ kind: z.literal("manifest") });
|
|
|
6024
5852
|
const manifestOutputSchema = z.discriminatedUnion("kind", [manifestJourneySchema.pick({
|
|
6025
5853
|
trigger: true,
|
|
6026
5854
|
purpose: true,
|
|
6027
|
-
|
|
5855
|
+
from: true,
|
|
6028
5856
|
tags: true
|
|
6029
5857
|
}).extend({ kind: z.literal("journey") }), manifestTemplateSchema.pick({
|
|
6030
5858
|
sendClass: true,
|
|
@@ -6088,7 +5916,7 @@ const executionDetailSchema = executionSchema.extend({
|
|
|
6088
5916
|
logs: z.array(executionLogSchema)
|
|
6089
5917
|
});
|
|
6090
5918
|
const listExecutionsQuerySchema = paginationQuerySchema.extend({
|
|
6091
|
-
/** A journey key
|
|
5919
|
+
/** A journey key. */
|
|
6092
5920
|
journey: z.string().trim().min(1).max(200).optional(),
|
|
6093
5921
|
/** One version's own runs, for the history rows on a journey. */
|
|
6094
5922
|
version: z.string().trim().min(1).max(200).optional(),
|
|
@@ -6104,12 +5932,15 @@ const listExecutionsQuerySchema = paginationQuerySchema.extend({
|
|
|
6104
5932
|
since: z.iso.datetime().optional()
|
|
6105
5933
|
});
|
|
6106
5934
|
/**
|
|
6107
|
-
* What to stop: every live execution of one journey
|
|
6108
|
-
*
|
|
6109
|
-
*
|
|
6110
|
-
*
|
|
5935
|
+
* What to stop: every live execution of one journey, or the ones still
|
|
5936
|
+
* pinned to one version. Cancelling is explicit (ADR 0011): disabling a
|
|
5937
|
+
* journey stops new entries and lets what is running finish, and this is
|
|
5938
|
+
* the other verb.
|
|
6111
5939
|
*/
|
|
6112
|
-
const cancelExecutionsBodySchema = z.object({ data: z.
|
|
5940
|
+
const cancelExecutionsBodySchema = z.object({ data: z.object({
|
|
5941
|
+
journey: z.string().trim().min(1).max(200).optional(),
|
|
5942
|
+
version: z.string().trim().min(1).max(200).optional()
|
|
5943
|
+
}).refine((data) => data.journey === void 0 !== (data.version === void 0), { message: "name a journey or a version, not both" }) });
|
|
6113
5944
|
/**
|
|
6114
5945
|
* Cancelling is a request, not an edit: each execution stops when it reaches
|
|
6115
5946
|
* its next step, so the answer is that the sweep is under way rather than a
|
|
@@ -6127,9 +5958,8 @@ const cancellingSchema = z.object({ cancelling: z.literal(true) });
|
|
|
6127
5958
|
const projectNameSchema = z.string().trim().min(1).max(200);
|
|
6128
5959
|
/**
|
|
6129
5960
|
* `cow.json` (spec: Project layout): the org, and which of its projects this
|
|
6130
|
-
* directory is.
|
|
6131
|
-
*
|
|
6132
|
-
* silently ignored setting.
|
|
5961
|
+
* directory is. Auth never lives in the project. Strict, so a typo'd key
|
|
5962
|
+
* is a build error rather than a silently ignored setting.
|
|
6133
5963
|
*/
|
|
6134
5964
|
const cowConfigSchema = z.strictObject({
|
|
6135
5965
|
$schema: z.url().optional(),
|
|
@@ -6238,7 +6068,7 @@ const createPushBodySchema = z.object({ data: z.object({
|
|
|
6238
6068
|
* is a live CLI talking to a live platform, so a mismatch here is a 422
|
|
6239
6069
|
* the developer fixes by updating `@cowliss/cli`, and nothing is stored.
|
|
6240
6070
|
*/
|
|
6241
|
-
manifest: manifestSchema.safeExtend({ protocol: z.literal(
|
|
6071
|
+
manifest: manifestSchema.safeExtend({ protocol: z.literal(3) }),
|
|
6242
6072
|
/** The `cow.json` project this push belongs to. */
|
|
6243
6073
|
project: projectNameSchema
|
|
6244
6074
|
}) });
|
|
@@ -6270,13 +6100,10 @@ const projectStatusKeySchema = z.object({
|
|
|
6270
6100
|
* server holds. Null only for a journey whose versions are all pruned.
|
|
6271
6101
|
*/
|
|
6272
6102
|
latestVersion: versionSchema.nullable(),
|
|
6273
|
-
/** The
|
|
6274
|
-
enabled:
|
|
6275
|
-
/** Executions still running or waiting
|
|
6276
|
-
liveExecutions: z.
|
|
6277
|
-
development: z.number().int(),
|
|
6278
|
-
production: z.number().int()
|
|
6279
|
-
})
|
|
6103
|
+
/** The journey's one flag; null on a template, which has none. */
|
|
6104
|
+
enabled: z.boolean().nullable(),
|
|
6105
|
+
/** Executions still running or waiting. */
|
|
6106
|
+
liveExecutions: z.number().int()
|
|
6280
6107
|
});
|
|
6281
6108
|
/**
|
|
6282
6109
|
* Everything the server knows about one project's keys, in one read: the
|
|
@@ -6285,17 +6112,14 @@ const projectStatusKeySchema = z.object({
|
|
|
6285
6112
|
* is my project doing" is useless split across cursors.
|
|
6286
6113
|
*
|
|
6287
6114
|
* `warnings` is what a deploy used to answer with (ADR 0011 retired the
|
|
6288
|
-
* deploy): a name a journey references that the
|
|
6289
|
-
* yet. They never block, because a segment or a
|
|
6115
|
+
* deploy): a name a journey references that the organization does not
|
|
6116
|
+
* define yet. They never block, because a segment or a webhook may be created
|
|
6290
6117
|
* right after a push.
|
|
6291
6118
|
*/
|
|
6292
6119
|
const projectStatusSchema = z.object({
|
|
6293
6120
|
project: projectNameSchema,
|
|
6294
6121
|
keys: z.array(projectStatusKeySchema),
|
|
6295
|
-
warnings: z.
|
|
6296
|
-
development: z.array(z.string()),
|
|
6297
|
-
production: z.array(z.string())
|
|
6298
|
-
})
|
|
6122
|
+
warnings: z.array(z.string())
|
|
6299
6123
|
});
|
|
6300
6124
|
|
|
6301
6125
|
//#endregion
|
|
@@ -6307,11 +6131,7 @@ const projectStatusSchema = z.object({
|
|
|
6307
6131
|
*
|
|
6308
6132
|
* Derived from the Drizzle table via drizzle-zod so the wire DTO and the row
|
|
6309
6133
|
* share one source of truth; the jsonb columns get explicit wire schemas
|
|
6310
|
-
* because the row types are opaque to drizzle-zod.
|
|
6311
|
-
* column: it is the journey_states value per environment, absent meaning
|
|
6312
|
-
* off, and both environments are reported whichever one the caller selected,
|
|
6313
|
-
* because "is this on in production?" is the question the list exists to
|
|
6314
|
-
* answer.
|
|
6134
|
+
* because the row types are opaque to drizzle-zod.
|
|
6315
6135
|
*/
|
|
6316
6136
|
const journeyTriggerSchema = triggerSchema;
|
|
6317
6137
|
/**
|
|
@@ -6324,7 +6144,7 @@ const journeySchema = selectJourneySchema.extend({
|
|
|
6324
6144
|
trigger: journeyTriggerSchema,
|
|
6325
6145
|
purpose: consentPurposeKeySchema,
|
|
6326
6146
|
spine: z.array(journeySpineEntrySchema),
|
|
6327
|
-
enabled:
|
|
6147
|
+
enabled: z.boolean(),
|
|
6328
6148
|
/**
|
|
6329
6149
|
* The newest version of this key, whatever it compiled to, so a list can
|
|
6330
6150
|
* say "pushed 2 hours ago" and "compiling" without a second read. What the
|
|
@@ -6343,7 +6163,7 @@ const journeySchema = selectJourneySchema.extend({
|
|
|
6343
6163
|
* Both are server-side, like every other list filter.
|
|
6344
6164
|
*/
|
|
6345
6165
|
const listJourneysQuerySchema = paginationQuerySchema.extend({
|
|
6346
|
-
/** On or off
|
|
6166
|
+
/** On or off; the journey's one flag. */
|
|
6347
6167
|
enabled: z.enum(["true", "false"]).optional(),
|
|
6348
6168
|
q: z.string().trim().max(200, "q must be at most 200 characters").optional().transform((value) => value ? value : void 0),
|
|
6349
6169
|
tag: z.string().trim().max(50, "tag must be at most 50 characters").optional()
|
|
@@ -6372,29 +6192,6 @@ const journeyStatsSchema = z.object({
|
|
|
6372
6192
|
deliveries: z.object({ byStatus: z.record(deliveryStatusSchema, z.number().int()) })
|
|
6373
6193
|
});
|
|
6374
6194
|
/**
|
|
6375
|
-
* A delivery row as the per-user journey timeline reports it. Deliberately
|
|
6376
|
-
* narrow: the full delivery DTO and its list contract live with the
|
|
6377
|
-
* deliveries log, and this timeline needs only what happened, not the
|
|
6378
|
-
* recipient address or the rendered payload.
|
|
6379
|
-
*/
|
|
6380
|
-
const journeyDeliverySchema = selectDeliverySchema.pick({
|
|
6381
|
-
id: true,
|
|
6382
|
-
journey: true,
|
|
6383
|
-
step: true,
|
|
6384
|
-
channel: true,
|
|
6385
|
-
status: true,
|
|
6386
|
-
dryRun: true,
|
|
6387
|
-
error: true
|
|
6388
|
-
}).extend({ createdAt: z.iso.datetime() });
|
|
6389
|
-
/** One journey on a user's timeline: the journey, their run, their sends. */
|
|
6390
|
-
const userJourneySchema = z.object({
|
|
6391
|
-
journey: journeySchema,
|
|
6392
|
-
/** Null when this user never entered the journey. */
|
|
6393
|
-
execution: executionSchema.nullable(),
|
|
6394
|
-
deliveries: z.array(journeyDeliverySchema)
|
|
6395
|
-
});
|
|
6396
|
-
const listUserJourneysQuerySchema = paginationQuerySchema;
|
|
6397
|
-
/**
|
|
6398
6195
|
* Which journeys the flag acts on: the keys the caller named, or every
|
|
6399
6196
|
* journey of one project. Exactly one of the two, because a call carrying
|
|
6400
6197
|
* both would have to decide which it meant.
|
|
@@ -6444,7 +6241,7 @@ const sandboxFailureCodeSchema = z.enum(SANDBOX_FAILURE_CODES);
|
|
|
6444
6241
|
* to be written by hand or by an agent, not generated.
|
|
6445
6242
|
*
|
|
6446
6243
|
* The schema lives here rather than in packages/journeys so the CLI can reject
|
|
6447
|
-
* a bad file before it boots a Temporal
|
|
6244
|
+
* a bad file before it boots a Temporal dev server, and so the docs site can
|
|
6448
6245
|
* render the format from one source.
|
|
6449
6246
|
*/
|
|
6450
6247
|
const journeyScenarioSchema = z.object({
|
|
@@ -6515,6 +6312,168 @@ const notificationPreferencesSchema = z.object({ purposes: z.array(notificationP
|
|
|
6515
6312
|
*/
|
|
6516
6313
|
const updateNotificationPreferencesBodySchema = z.object({ data: z.object({ purposes: consentPatchSchema }) });
|
|
6517
6314
|
|
|
6315
|
+
//#endregion
|
|
6316
|
+
//#region ../../packages/shared/src/resend.ts
|
|
6317
|
+
/** Resend's own cap on one message's recipients, per list. */
|
|
6318
|
+
const RESEND_MAX_RECIPIENTS = 50;
|
|
6319
|
+
/** Resend's own cap on one batch. */
|
|
6320
|
+
const RESEND_MAX_BATCH = 100;
|
|
6321
|
+
/** Resend's own cap on a from-header, a subject, and a message's tags. */
|
|
6322
|
+
const RESEND_MAX_FROM = 200;
|
|
6323
|
+
const RESEND_MAX_SUBJECT = 1e3;
|
|
6324
|
+
const RESEND_MAX_TAGS = 50;
|
|
6325
|
+
/**
|
|
6326
|
+
* One recipient: a bare address or `Name <addr@domain>`, both of which SES
|
|
6327
|
+
* takes in a Destination. The same parser the journey path uses, so an
|
|
6328
|
+
* address the facade accepts is one a journey could have sent to.
|
|
6329
|
+
*/
|
|
6330
|
+
const recipientSchema = z.string().trim().max(320).refine((value) => parseFromAddress(value) !== null, { message: "Every recipient must be an address (\"ada@acme.com\") or a name and address (\"Ada <ada@acme.com>\")." });
|
|
6331
|
+
const recipientArraySchema = z.array(recipientSchema).min(1).max(50);
|
|
6332
|
+
/** Resend takes one address or a list wherever it takes recipients. */
|
|
6333
|
+
const recipientsSchema = z.union([recipientSchema, recipientArraySchema]);
|
|
6334
|
+
/**
|
|
6335
|
+
* Resend's wording for an absent required field. Three fields can be
|
|
6336
|
+
* missing: `to`, `subject`, and a body. Which answer each one gets is read
|
|
6337
|
+
* off the issue itself, never off this copy, so rewording any of them is
|
|
6338
|
+
* safe (see `isMissingField`).
|
|
6339
|
+
*/
|
|
6340
|
+
const MISSING = (field) => `Missing \`${field}\` field.`;
|
|
6341
|
+
/**
|
|
6342
|
+
* A field Cowliss does not serve. Declared rather than left to the strict
|
|
6343
|
+
* object's "unrecognized key" so the 422 names the field and says what to
|
|
6344
|
+
* do instead; the Resend SDK omits an unset one, so only a caller that
|
|
6345
|
+
* really passed it is refused.
|
|
6346
|
+
*
|
|
6347
|
+
* `any` and a refusing check rather than `never`, which the OpenAPI
|
|
6348
|
+
* generator has no rendering for.
|
|
6349
|
+
*/
|
|
6350
|
+
const unsupported = (message) => z.any().refine(() => false, { error: message }).optional();
|
|
6351
|
+
/**
|
|
6352
|
+
* Resend's template slot, pointing at a Cowliss template key (ADR 0014).
|
|
6353
|
+
* `variables` are the template's own props: they are checked by the
|
|
6354
|
+
* template, in the sandbox, the same way a test send's are, so anything
|
|
6355
|
+
* JSON can carry is accepted here and the template decides.
|
|
6356
|
+
*/
|
|
6357
|
+
const templateSlotSchema = z.strictObject({
|
|
6358
|
+
id: journeyKeySchema,
|
|
6359
|
+
variables: z.record(z.string(), z.unknown()).optional()
|
|
6360
|
+
});
|
|
6361
|
+
/** Send body: Resend's fields, minus the ones Cowliss does not serve. */
|
|
6362
|
+
const resendSendBodySchema = z.strictObject({
|
|
6363
|
+
/**
|
|
6364
|
+
* Optional here where Resend requires it: an omitted `from` is the
|
|
6365
|
+
* organization's shared fallback address, which is what makes a first
|
|
6366
|
+
* send work with no configuration at all.
|
|
6367
|
+
*/
|
|
6368
|
+
from: z.string().trim().max(200).refine((value) => parseFromAddress(value) !== null, { message: "The from address must be an address (\"billing@acme.com\") or a name and address (\"Billing <billing@acme.com>\")." }).optional(),
|
|
6369
|
+
/**
|
|
6370
|
+
* Required, and the absence is its own issue rather than the union's:
|
|
6371
|
+
* `optional().nonoptional()` lets the value through to the union when
|
|
6372
|
+
* there is one and answers for undefined itself, which is how
|
|
6373
|
+
* `isMissingField` tells "you left it out" from "you sent the wrong
|
|
6374
|
+
* thing" without reading the message.
|
|
6375
|
+
*/
|
|
6376
|
+
to: z.union([recipientSchema, recipientArraySchema]).optional().nonoptional({ error: MISSING("to") }),
|
|
6377
|
+
/**
|
|
6378
|
+
* Optional in the type and required in practice: a message either
|
|
6379
|
+
* carries its own subject or names a template, which brings one. The
|
|
6380
|
+
* refinement below says which of the two is missing.
|
|
6381
|
+
*/
|
|
6382
|
+
subject: z.string().trim().min(1).max(RESEND_MAX_SUBJECT).optional(),
|
|
6383
|
+
html: z.string().optional(),
|
|
6384
|
+
text: z.string().optional(),
|
|
6385
|
+
cc: recipientsSchema.optional(),
|
|
6386
|
+
bcc: recipientsSchema.optional(),
|
|
6387
|
+
reply_to: recipientsSchema.optional(),
|
|
6388
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
6389
|
+
/** Accepted and dropped: Cowliss tags a delivery by its own fields. */
|
|
6390
|
+
tags: z.array(z.object({
|
|
6391
|
+
name: z.string(),
|
|
6392
|
+
value: z.string()
|
|
6393
|
+
})).max(50).optional(),
|
|
6394
|
+
attachments: unsupported("Cowliss does not accept attachments. Link to the file from the message instead."),
|
|
6395
|
+
scheduled_at: unsupported("Cowliss does not schedule sends. Send the message when it should arrive, or let a journey's wait step do the waiting."),
|
|
6396
|
+
topic_id: unsupported("Cowliss does not take topics. Consent purposes are where it records what a person agreed to."),
|
|
6397
|
+
template: templateSlotSchema.optional()
|
|
6398
|
+
}).superRefine((body, ctx) => {
|
|
6399
|
+
const refuse = (path, message) => ctx.addIssue({
|
|
6400
|
+
code: "custom",
|
|
6401
|
+
path: [path],
|
|
6402
|
+
message
|
|
6403
|
+
});
|
|
6404
|
+
const missing = (path, message) => ctx.addIssue({
|
|
6405
|
+
code: "custom",
|
|
6406
|
+
path: [path],
|
|
6407
|
+
message,
|
|
6408
|
+
params: { missing: true }
|
|
6409
|
+
});
|
|
6410
|
+
if (body.template) {
|
|
6411
|
+
if (body.subject !== void 0) refuse("subject", "The subject comes from the template. Remove `subject`, or write the message yourself with `html` or `text`.");
|
|
6412
|
+
for (const field of ["html", "text"]) if (body[field] !== void 0) refuse(field, `A template brings its own content. Remove \`${field}\`, or drop \`template\` and write the message yourself.`);
|
|
6413
|
+
return;
|
|
6414
|
+
}
|
|
6415
|
+
if (body.subject === void 0) missing("subject", MISSING("subject"));
|
|
6416
|
+
if (body.html === void 0 && body.text === void 0) missing("html", "Missing `html` or `text` field.");
|
|
6417
|
+
});
|
|
6418
|
+
/**
|
|
6419
|
+
* A batch: Resend posts the messages as a bare array, and answers with one
|
|
6420
|
+
* id per message in the order they were given. Every message is checked
|
|
6421
|
+
* before any of them is sent, so a batch with a mistake in it sends
|
|
6422
|
+
* nothing.
|
|
6423
|
+
*/
|
|
6424
|
+
const resendBatchBodySchema = z.array(resendSendBodySchema, { error: (issue) => Array.isArray(issue.input) ? void 0 : "Send a list of messages." }).min(1, "Send at least one message.").max(100, `A batch takes at most ${100} messages. Split the list and send it in parts.`);
|
|
6425
|
+
/** What a send answers: the delivery's id, and nothing else. */
|
|
6426
|
+
const resendSentSchema = z.object({ id: z.string() });
|
|
6427
|
+
/** What a batch answers: one id per message, in the order they were given. */
|
|
6428
|
+
const resendBatchSentSchema = z.object({ data: z.array(resendSentSchema) });
|
|
6429
|
+
/**
|
|
6430
|
+
* Where a message got to, in Resend's vocabulary. Cowliss emits five of
|
|
6431
|
+
* them: a gate skip and a failure are both `failed`, because to a caller
|
|
6432
|
+
* they are the same fact, the message did not go.
|
|
6433
|
+
*/
|
|
6434
|
+
const RESEND_LAST_EVENTS = [
|
|
6435
|
+
"sent",
|
|
6436
|
+
"delivered",
|
|
6437
|
+
"bounced",
|
|
6438
|
+
"complained",
|
|
6439
|
+
"failed"
|
|
6440
|
+
];
|
|
6441
|
+
/**
|
|
6442
|
+
* Resend's email object. `html` and `text` are always null: Cowliss stores
|
|
6443
|
+
* no rendered bodies (ADR 0014), and the SDK does not need them to report
|
|
6444
|
+
* on a send.
|
|
6445
|
+
*/
|
|
6446
|
+
const resendEmailSchema = z.object({
|
|
6447
|
+
object: z.literal("email"),
|
|
6448
|
+
id: z.string(),
|
|
6449
|
+
/** The provider's message id; null until the message reaches the provider. */
|
|
6450
|
+
message_id: z.string().nullable(),
|
|
6451
|
+
to: z.array(z.string()),
|
|
6452
|
+
from: z.string(),
|
|
6453
|
+
created_at: z.string(),
|
|
6454
|
+
subject: z.string(),
|
|
6455
|
+
html: z.null(),
|
|
6456
|
+
text: z.null(),
|
|
6457
|
+
bcc: z.array(z.string()),
|
|
6458
|
+
cc: z.array(z.string()),
|
|
6459
|
+
reply_to: z.array(z.string()),
|
|
6460
|
+
last_event: z.enum(RESEND_LAST_EVENTS),
|
|
6461
|
+
scheduled_at: z.null(),
|
|
6462
|
+
tags: z.array(z.object({
|
|
6463
|
+
name: z.string(),
|
|
6464
|
+
value: z.string()
|
|
6465
|
+
}))
|
|
6466
|
+
});
|
|
6467
|
+
/**
|
|
6468
|
+
* Resend's error body. `name` is the machine-readable one, which is what an
|
|
6469
|
+
* SDK caller branches on; the message is for the developer reading it.
|
|
6470
|
+
*/
|
|
6471
|
+
const resendErrorSchema = z.object({
|
|
6472
|
+
statusCode: z.number(),
|
|
6473
|
+
message: z.string(),
|
|
6474
|
+
name: z.string()
|
|
6475
|
+
});
|
|
6476
|
+
|
|
6518
6477
|
//#endregion
|
|
6519
6478
|
//#region ../../packages/shared/src/violations.ts
|
|
6520
6479
|
/**
|
|
@@ -6625,9 +6584,8 @@ const listReviewQuerySchema = paginationQuerySchema.extend({
|
|
|
6625
6584
|
* per-profile and profiles merge across apps, so there is nothing
|
|
6626
6585
|
* app-shaped to filter on the trait side.
|
|
6627
6586
|
*
|
|
6628
|
-
* A definition
|
|
6629
|
-
*
|
|
6630
|
-
* definition that runs in both names both of an app's ids.
|
|
6587
|
+
* A definition and the membership it produces are both the
|
|
6588
|
+
* organization's.
|
|
6631
6589
|
*
|
|
6632
6590
|
* The object is strict: a definition holding the retired `sourceId` key
|
|
6633
6591
|
* (which meant the app) fails loudly instead of parsing as an unfiltered
|
|
@@ -6697,7 +6655,6 @@ const segmentDefinitionSchema = z.strictObject({
|
|
|
6697
6655
|
});
|
|
6698
6656
|
const segmentSchema = selectSegmentSchema.extend({
|
|
6699
6657
|
definition: segmentDefinitionSchema,
|
|
6700
|
-
environments: environmentsSchema,
|
|
6701
6658
|
createdAt: z.iso.datetime(),
|
|
6702
6659
|
updatedAt: z.iso.datetime()
|
|
6703
6660
|
});
|
|
@@ -6733,41 +6690,33 @@ const descriptionSchema = z.string().trim().max(500, "description must be at mos
|
|
|
6733
6690
|
const createSegmentBodySchema = z.object({ data: z.object({
|
|
6734
6691
|
name: nameSchema,
|
|
6735
6692
|
description: descriptionSchema.nullish(),
|
|
6736
|
-
definition: segmentDefinitionSchema
|
|
6737
|
-
/** The environments the segment works on; both when omitted. */
|
|
6738
|
-
environments: environmentsSchema.default([...ENVIRONMENTS])
|
|
6693
|
+
definition: segmentDefinitionSchema
|
|
6739
6694
|
}) });
|
|
6740
6695
|
const updateSegmentBodySchema = z.object({ data: z.object({
|
|
6741
6696
|
name: nameSchema.optional(),
|
|
6742
6697
|
description: descriptionSchema.nullish(),
|
|
6743
|
-
definition: segmentDefinitionSchema.optional()
|
|
6744
|
-
|
|
6745
|
-
environments: environmentsSchema.optional()
|
|
6746
|
-
}).refine((data) => data.name !== void 0 || data.description !== void 0 || data.definition !== void 0 || data.environments !== void 0, { message: "at least one field (name, description, definition or environments) is required" }) });
|
|
6698
|
+
definition: segmentDefinitionSchema.optional()
|
|
6699
|
+
}).refine((data) => data.name !== void 0 || data.description !== void 0 || data.definition !== void 0, { message: "at least one field (name, description or definition) is required" }) });
|
|
6747
6700
|
/** `q` is a substring search over the segment's name. */
|
|
6748
6701
|
const listSegmentsQuerySchema = paginationQuerySchema.extend({ q: searchQuerySchema });
|
|
6749
|
-
const listSegmentMembersQuerySchema = paginationQuerySchema;
|
|
6702
|
+
const listSegmentMembersQuerySchema = paginationQuerySchema.extend({});
|
|
6750
6703
|
|
|
6751
6704
|
//#endregion
|
|
6752
6705
|
//#region ../../packages/shared/src/settings.ts
|
|
6753
6706
|
/**
|
|
6754
6707
|
* Settings contracts. The two operator-set values, ingestionPolicy (catalog
|
|
6755
|
-
* governance) and eventRetentionDays (the ClickHouse TTL window), are
|
|
6756
|
-
*
|
|
6757
|
-
*
|
|
6758
|
-
* The org's Clerk profile rides along read-only.
|
|
6708
|
+
* governance) and eventRetentionDays (the ClickHouse TTL window), are the
|
|
6709
|
+
* organization's, one each: `GET/PATCH /v1/settings/org` read and write
|
|
6710
|
+
* them. The org's Clerk profile rides along read-only.
|
|
6759
6711
|
*/
|
|
6760
6712
|
const ingestionPolicySchema = z.enum(INGESTION_POLICIES);
|
|
6761
6713
|
/**
|
|
6762
|
-
* The
|
|
6763
|
-
*
|
|
6764
|
-
*
|
|
6765
|
-
* selected environment's own (30 days in development) with a 422.
|
|
6714
|
+
* The org's event retention window in days. Never null: an unset window is
|
|
6715
|
+
* the default, and there is no keep-forever. Both default and cap are 18
|
|
6716
|
+
* months.
|
|
6766
6717
|
*/
|
|
6767
|
-
const eventRetentionDaysSchema = z.number().int().min(1).max(
|
|
6718
|
+
const eventRetentionDaysSchema = z.number().int().min(1).max(548);
|
|
6768
6719
|
const orgSettingsDtoSchema = z.object({
|
|
6769
|
-
/** The environment these values belong to (from the selection header). */
|
|
6770
|
-
environment: environmentSchema,
|
|
6771
6720
|
ingestionPolicy: ingestionPolicySchema,
|
|
6772
6721
|
eventRetentionDays: eventRetentionDaysSchema,
|
|
6773
6722
|
/**
|
|
@@ -6781,9 +6730,8 @@ const orgSettingsDtoSchema = z.object({
|
|
|
6781
6730
|
name: z.string().nullable()
|
|
6782
6731
|
});
|
|
6783
6732
|
/**
|
|
6784
|
-
* Settings patch
|
|
6785
|
-
*
|
|
6786
|
-
* patch is a no-op request, not a valid one.
|
|
6733
|
+
* Settings patch. Each field is absent-means-no-change. At least one field
|
|
6734
|
+
* must be present: an empty patch is a no-op request, not a valid one.
|
|
6787
6735
|
*/
|
|
6788
6736
|
const updateOrgSettingsBodySchema = z.object({ data: z.object({
|
|
6789
6737
|
ingestionPolicy: ingestionPolicySchema.optional(),
|
|
@@ -6825,7 +6773,7 @@ const sourceSchema = selectSourceSchema.extend({
|
|
|
6825
6773
|
*/
|
|
6826
6774
|
const SOURCE_KINDS = sourceKindEnum.enumValues;
|
|
6827
6775
|
/**
|
|
6828
|
-
* The per-kind source config schemas, mirroring the
|
|
6776
|
+
* The per-kind source config schemas, mirroring the webhooks
|
|
6829
6777
|
* discriminated-union pattern. The `api` source has nothing to configure:
|
|
6830
6778
|
* ingestion authenticates with the org-level API key, so the row carries no
|
|
6831
6779
|
* credential of its own. A Clerk source carries only the webhook signing
|
|
@@ -6842,7 +6790,7 @@ const apiSourceConfigSchema = z.strictObject({}, { error: "an api source takes n
|
|
|
6842
6790
|
const clerkSourceConfigSchema = z.object({ signingSecret: z.string().min(1, "config.signingSecret is required") });
|
|
6843
6791
|
/**
|
|
6844
6792
|
* The source shapes, named individually so the dashboard's create form can
|
|
6845
|
-
* resolve against exactly one of them (the
|
|
6793
|
+
* resolve against exactly one of them (the webhooks pattern: a form knows
|
|
6846
6794
|
* its kind before the user types anything, and zodResolver over a member is
|
|
6847
6795
|
* what gives react-hook-form a non-union field path to register).
|
|
6848
6796
|
*
|
|
@@ -6874,7 +6822,7 @@ const listSourcesQuerySchema = paginationQuerySchema;
|
|
|
6874
6822
|
//#region ../../packages/shared/src/suppressions.ts
|
|
6875
6823
|
/**
|
|
6876
6824
|
* The suppression mirror's wire contracts. Derived from the Drizzle table so
|
|
6877
|
-
* the row and the DTO cannot drift, same as deliveries and
|
|
6825
|
+
* the row and the DTO cannot drift, same as deliveries and webhooks.
|
|
6878
6826
|
*/
|
|
6879
6827
|
const SUPPRESSION_TYPES = suppressionTypeEnum.enumValues;
|
|
6880
6828
|
const suppressionTypeSchema = z.enum(SUPPRESSION_TYPES);
|
|
@@ -6897,7 +6845,7 @@ const listSuppressionsQuerySchema = paginationQuerySchema.extend({ q: searchQuer
|
|
|
6897
6845
|
//#region ../../packages/shared/src/templates.ts
|
|
6898
6846
|
/**
|
|
6899
6847
|
* Templates as the API reports them. A template has versions and nothing
|
|
6900
|
-
* else (ADR 0011): no row of its own, no
|
|
6848
|
+
* else (ADR 0011): no row of its own, no flag. What is
|
|
6901
6849
|
* reported here is one key, described by its newest version, plus the
|
|
6902
6850
|
* journeys whose steps send it.
|
|
6903
6851
|
*
|
|
@@ -6932,6 +6880,8 @@ const templateSchema = z.object({
|
|
|
6932
6880
|
const listTemplatesQuerySchema = paginationQuerySchema.extend({ q: z.string().trim().max(200, "q must be at most 200 characters").optional().transform((value) => value ? value : void 0) });
|
|
6933
6881
|
/** The props a preview or a test send renders the template with. */
|
|
6934
6882
|
const renderTemplateBodySchema = z.object({ data: z.object({ props: z.record(z.string(), z.unknown()).default({}) }) });
|
|
6883
|
+
/** A test send is a delivery like any other, so it takes the same props. */
|
|
6884
|
+
const testSendTemplateBodySchema = z.object({ data: z.object({ props: z.record(z.string(), z.unknown()).default({}) }) });
|
|
6935
6885
|
/** What the sandbox rendered: the message as it would go out. */
|
|
6936
6886
|
const templatePreviewSchema = z.object({
|
|
6937
6887
|
subject: z.string(),
|
|
@@ -6945,9 +6895,8 @@ const templatePreviewSchema = z.object({
|
|
|
6945
6895
|
//#region ../../packages/shared/src/users.ts
|
|
6946
6896
|
/**
|
|
6947
6897
|
* Users API contracts (dashboard). Profiles are addressed by their
|
|
6948
|
-
* Cowliss-generated id (`usr_`)
|
|
6949
|
-
*
|
|
6950
|
-
* profile's event history is the event feed filtered to it
|
|
6898
|
+
* Cowliss-generated id (`usr_`). A profile's event history is the event
|
|
6899
|
+
* feed filtered to it
|
|
6951
6900
|
* (GET /v1/events?profileId=), not an endpoint of its own.
|
|
6952
6901
|
*/
|
|
6953
6902
|
/**
|
|
@@ -7039,6 +6988,62 @@ const userExportSchema = z.object({
|
|
|
7039
6988
|
quarantineEntries: z.array(quarantineEntrySchema)
|
|
7040
6989
|
});
|
|
7041
6990
|
|
|
6991
|
+
//#endregion
|
|
6992
|
+
//#region ../../packages/shared/src/webhooks.ts
|
|
6993
|
+
/**
|
|
6994
|
+
* Webhooks API contracts. Derived from the Drizzle table via drizzle-zod.
|
|
6995
|
+
* signingSecret is deliberately absent from the DTO: it is shown exactly
|
|
6996
|
+
* once, in the create response (webhookCreatedSchema). consecutiveFailures
|
|
6997
|
+
* is absent too: it is the auto-disable counter's internal state, and
|
|
6998
|
+
* disabledAt is the part the dashboard banner needs.
|
|
6999
|
+
*
|
|
7000
|
+
* A webhook is the organization's: the name is unique per org, and every
|
|
7001
|
+
* execution posts to the same row.
|
|
7002
|
+
*/
|
|
7003
|
+
/**
|
|
7004
|
+
* A webhook's name: the token a journey addresses it by, in a `send.webhook`
|
|
7005
|
+
* call. The journey names a string and the server resolves it, so the two
|
|
7006
|
+
* ends only have to agree on the characters that fit in one.
|
|
7007
|
+
*/
|
|
7008
|
+
const webhookNameSchema = z.string().trim().min(1, "name is required").max(100, "name must be at most 100 characters");
|
|
7009
|
+
const webhookConfigSchema = z.object({ url: z.url("config.url must be a valid URL").refine((url) => {
|
|
7010
|
+
try {
|
|
7011
|
+
return ["http:", "https:"].includes(new URL(url).protocol);
|
|
7012
|
+
} catch {
|
|
7013
|
+
return false;
|
|
7014
|
+
}
|
|
7015
|
+
}, { message: "config.url must use http or https" }) });
|
|
7016
|
+
const webhookSchema = selectWebhookSchema.extend({
|
|
7017
|
+
config: webhookConfigSchema,
|
|
7018
|
+
createdAt: z.iso.datetime(),
|
|
7019
|
+
updatedAt: z.iso.datetime(),
|
|
7020
|
+
disabledAt: z.iso.datetime().nullable()
|
|
7021
|
+
}).omit({
|
|
7022
|
+
signingSecret: true,
|
|
7023
|
+
consecutiveFailures: true
|
|
7024
|
+
});
|
|
7025
|
+
const webhookCreatedSchema = webhookSchema.extend({ signingSecret: z.string() });
|
|
7026
|
+
const createWebhookBodySchema = z.object({ data: z.object({
|
|
7027
|
+
name: webhookNameSchema,
|
|
7028
|
+
config: webhookConfigSchema
|
|
7029
|
+
}) });
|
|
7030
|
+
/**
|
|
7031
|
+
* Webhook update. `enabled` is the admin's half of the auto-disable loop:
|
|
7032
|
+
* the webhook send activity stamps `disabledAt` after enough consecutive
|
|
7033
|
+
* failures and every send after that records `skipped_disabled`, so without
|
|
7034
|
+
* a way to clear it the only exit would be deleting the webhook, which
|
|
7035
|
+
* throws away the signing secret the receiver is configured with.
|
|
7036
|
+
* `enabled: true` clears the stamp and the failure counter; `enabled: false`
|
|
7037
|
+
* is the same switch operated by hand.
|
|
7038
|
+
*/
|
|
7039
|
+
const updateWebhookBodySchema = z.object({ data: z.object({
|
|
7040
|
+
name: webhookNameSchema.optional(),
|
|
7041
|
+
config: webhookConfigSchema.optional(),
|
|
7042
|
+
enabled: z.boolean().optional()
|
|
7043
|
+
}).refine((data) => data.name !== void 0 || data.config !== void 0 || data.enabled !== void 0, { message: "at least one field (name, config, or enabled) is required" }) });
|
|
7044
|
+
/** `q` is a substring search over the webhook's name. */
|
|
7045
|
+
const listWebhooksQuerySchema = paginationQuerySchema.extend({ q: searchQuerySchema });
|
|
7046
|
+
|
|
7042
7047
|
//#endregion
|
|
7043
7048
|
//#region ../../packages/api-client/src/client.ts
|
|
7044
7049
|
var ApiError = class extends Error {
|
|
@@ -7222,7 +7227,7 @@ function readCliPackage() {
|
|
|
7222
7227
|
* are. Reading the code sees every branch, labelled with its condition.
|
|
7223
7228
|
*
|
|
7224
7229
|
* What it reads: `await api.*` calls (in source order, with the literal
|
|
7225
|
-
* template,
|
|
7230
|
+
* template, webhook, event, key, or duration when the author wrote one
|
|
7226
7231
|
* inline), `if`/`else`, `switch`, loops, `try`/`catch`, `return`, `throw`, and
|
|
7227
7232
|
* `api.restart()`. Calls behind a helper function in another file, and a
|
|
7228
7233
|
* `run` that is not an inline function on the `defineJourney` object, are
|
|
@@ -7433,12 +7438,9 @@ var Reader = class {
|
|
|
7433
7438
|
if (timeout) entry.timeout = timeout;
|
|
7434
7439
|
break;
|
|
7435
7440
|
}
|
|
7436
|
-
case "send.email":
|
|
7441
|
+
case "send.email":
|
|
7437
7442
|
detail(this.property(first, "template"));
|
|
7438
|
-
const sender = this.property(first, "senderIdentity");
|
|
7439
|
-
if (sender) entry.senderIdentity = sender;
|
|
7440
7443
|
break;
|
|
7441
|
-
}
|
|
7442
7444
|
case "send.webhook":
|
|
7443
7445
|
detail(this.property(first, "destination"));
|
|
7444
7446
|
break;
|
|
@@ -7999,7 +8001,7 @@ async function buildProject(projectDir) {
|
|
|
7999
8001
|
tags: report.tags,
|
|
8000
8002
|
trigger: report.trigger,
|
|
8001
8003
|
purpose: report.purpose,
|
|
8002
|
-
|
|
8004
|
+
from: report.from,
|
|
8003
8005
|
spine: readSpine(await readFile(built.source.file, "utf8")),
|
|
8004
8006
|
bundle: built.digest
|
|
8005
8007
|
});
|
|
@@ -8025,7 +8027,7 @@ async function buildProject(projectDir) {
|
|
|
8025
8027
|
});
|
|
8026
8028
|
if (failure) throw new Error(failure);
|
|
8027
8029
|
const manifest = manifestSchema.parse({
|
|
8028
|
-
protocol:
|
|
8030
|
+
protocol: 3,
|
|
8029
8031
|
sdk: (await readCliPackage()).version,
|
|
8030
8032
|
journeys: manifestJourneys,
|
|
8031
8033
|
templates: manifestTemplates,
|
|
@@ -9264,82 +9266,9 @@ all: z.literal("1").optional() }) },
|
|
|
9264
9266
|
}
|
|
9265
9267
|
}));
|
|
9266
9268
|
|
|
9267
|
-
//#endregion
|
|
9268
|
-
//#region ../../packages/shared/src/contract/destinations.ts
|
|
9269
|
-
const params$9 = z.object({ id: z.string() });
|
|
9270
|
-
const destinations = defineModule(defineRoute({
|
|
9271
|
-
method: "post",
|
|
9272
|
-
path: "/v1/destinations",
|
|
9273
|
-
operationId: "destinations.create",
|
|
9274
|
-
tags: ["destinations"],
|
|
9275
|
-
summary: "Create a destination",
|
|
9276
|
-
security: SESSION_AUTH,
|
|
9277
|
-
request: { body: jsonBody(createDestinationBodySchema) },
|
|
9278
|
-
responses: {
|
|
9279
|
-
201: envelope(destinationCreatedSchema, "The created destination; the only response carrying the signing secret"),
|
|
9280
|
-
...sessionErrors,
|
|
9281
|
-
...errors("validation_failed", "malformed_request")
|
|
9282
|
-
}
|
|
9283
|
-
}), defineRoute({
|
|
9284
|
-
method: "get",
|
|
9285
|
-
path: "/v1/destinations",
|
|
9286
|
-
operationId: "destinations.list",
|
|
9287
|
-
tags: ["destinations"],
|
|
9288
|
-
summary: "List destinations in the selected environment",
|
|
9289
|
-
security: SESSION_AUTH,
|
|
9290
|
-
request: { query: listDestinationsQuerySchema },
|
|
9291
|
-
responses: {
|
|
9292
|
-
200: list(destinationSchema),
|
|
9293
|
-
...sessionErrors,
|
|
9294
|
-
...errors("validation_failed")
|
|
9295
|
-
}
|
|
9296
|
-
}), defineRoute({
|
|
9297
|
-
method: "get",
|
|
9298
|
-
path: "/v1/destinations/{id}",
|
|
9299
|
-
operationId: "destinations.get",
|
|
9300
|
-
tags: ["destinations"],
|
|
9301
|
-
summary: "Fetch one destination",
|
|
9302
|
-
security: SESSION_AUTH,
|
|
9303
|
-
request: { params: params$9 },
|
|
9304
|
-
responses: {
|
|
9305
|
-
200: envelope(destinationSchema),
|
|
9306
|
-
...sessionErrors,
|
|
9307
|
-
...errors("not_found")
|
|
9308
|
-
}
|
|
9309
|
-
}), defineRoute({
|
|
9310
|
-
method: "patch",
|
|
9311
|
-
path: "/v1/destinations/{id}",
|
|
9312
|
-
operationId: "destinations.update",
|
|
9313
|
-
tags: ["destinations"],
|
|
9314
|
-
summary: "Update a destination",
|
|
9315
|
-
security: SESSION_AUTH,
|
|
9316
|
-
request: {
|
|
9317
|
-
params: params$9,
|
|
9318
|
-
body: jsonBody(updateDestinationBodySchema)
|
|
9319
|
-
},
|
|
9320
|
-
responses: {
|
|
9321
|
-
200: envelope(destinationSchema),
|
|
9322
|
-
...sessionErrors,
|
|
9323
|
-
...errors("not_found", "validation_failed", "malformed_request")
|
|
9324
|
-
}
|
|
9325
|
-
}), defineRoute({
|
|
9326
|
-
method: "delete",
|
|
9327
|
-
path: "/v1/destinations/{id}",
|
|
9328
|
-
operationId: "destinations.delete",
|
|
9329
|
-
tags: ["destinations"],
|
|
9330
|
-
summary: "Delete a destination",
|
|
9331
|
-
security: SESSION_AUTH,
|
|
9332
|
-
request: { params: params$9 },
|
|
9333
|
-
responses: {
|
|
9334
|
-
200: envelope(deletedSchema),
|
|
9335
|
-
...sessionErrors,
|
|
9336
|
-
...errors("not_found")
|
|
9337
|
-
}
|
|
9338
|
-
}));
|
|
9339
|
-
|
|
9340
9269
|
//#endregion
|
|
9341
9270
|
//#region ../../packages/shared/src/contract/domains.ts
|
|
9342
|
-
const params$
|
|
9271
|
+
const params$9 = z.object({ id: z.string() });
|
|
9343
9272
|
const domains = defineModule(defineRoute({
|
|
9344
9273
|
method: "get",
|
|
9345
9274
|
path: "/v1/domains",
|
|
@@ -9360,7 +9289,7 @@ const domains = defineModule(defineRoute({
|
|
|
9360
9289
|
tags: ["domains"],
|
|
9361
9290
|
summary: "Fetch one sending domain",
|
|
9362
9291
|
security: SESSION_AUTH,
|
|
9363
|
-
request: { params: params$
|
|
9292
|
+
request: { params: params$9 },
|
|
9364
9293
|
responses: {
|
|
9365
9294
|
200: envelope(senderDomainSchema),
|
|
9366
9295
|
...sessionErrors,
|
|
@@ -9388,7 +9317,7 @@ const domains = defineModule(defineRoute({
|
|
|
9388
9317
|
description: "The Domain Connect link for this domain's DNS provider, which shows the admin the records and asks them to confirm. `url` is null when the provider does not support it.",
|
|
9389
9318
|
security: SESSION_AUTH,
|
|
9390
9319
|
surfaces: HIDDEN_FROM_TOOLS,
|
|
9391
|
-
request: { params: params$
|
|
9320
|
+
request: { params: params$9 },
|
|
9392
9321
|
responses: {
|
|
9393
9322
|
200: envelope(domainDnsSetupSchema),
|
|
9394
9323
|
...sessionErrors,
|
|
@@ -9401,7 +9330,7 @@ const domains = defineModule(defineRoute({
|
|
|
9401
9330
|
tags: ["domains"],
|
|
9402
9331
|
summary: "Re-check DNS now",
|
|
9403
9332
|
security: SESSION_AUTH,
|
|
9404
|
-
request: { params: params$
|
|
9333
|
+
request: { params: params$9 },
|
|
9405
9334
|
responses: {
|
|
9406
9335
|
200: envelope(senderDomainSchema),
|
|
9407
9336
|
...sessionErrors,
|
|
@@ -9415,7 +9344,7 @@ const domains = defineModule(defineRoute({
|
|
|
9415
9344
|
summary: "Toggle click tracking",
|
|
9416
9345
|
security: SESSION_AUTH,
|
|
9417
9346
|
request: {
|
|
9418
|
-
params: params$
|
|
9347
|
+
params: params$9,
|
|
9419
9348
|
body: jsonBody(updateSenderDomainBodySchema)
|
|
9420
9349
|
},
|
|
9421
9350
|
responses: {
|
|
@@ -9430,7 +9359,7 @@ const domains = defineModule(defineRoute({
|
|
|
9430
9359
|
tags: ["domains"],
|
|
9431
9360
|
summary: "Give up a domain claim",
|
|
9432
9361
|
security: SESSION_AUTH,
|
|
9433
|
-
request: { params: params$
|
|
9362
|
+
request: { params: params$9 },
|
|
9434
9363
|
responses: {
|
|
9435
9364
|
200: envelope(deletedSchema),
|
|
9436
9365
|
...sessionErrors,
|
|
@@ -9528,15 +9457,15 @@ const events = defineModule(defineRoute({
|
|
|
9528
9457
|
|
|
9529
9458
|
//#endregion
|
|
9530
9459
|
//#region ../../packages/shared/src/contract/executions.ts
|
|
9531
|
-
const params$
|
|
9460
|
+
const params$8 = z.object({ id: z.string() });
|
|
9532
9461
|
/**
|
|
9533
9462
|
* Executions: one run of one journey for one profile. Replaces v1's journey
|
|
9534
9463
|
* instances, which were read live from Temporal and so could only be listed
|
|
9535
9464
|
* per journey; these are rows, so they filter by journey, version, status,
|
|
9536
|
-
* and profile in one place.
|
|
9465
|
+
* and profile in one place.
|
|
9537
9466
|
*
|
|
9538
|
-
* `cancel` is the only write: stopping is explicit and
|
|
9539
|
-
*
|
|
9467
|
+
* `cancel` is the only write: stopping is explicit (ADR 0011), and each
|
|
9468
|
+
* execution records `cancelled` when it reaches its
|
|
9540
9469
|
* next step, so the answer is that the sweep started rather than a count.
|
|
9541
9470
|
* A pipeline may push, enable, disable and READ executions; stopping runs
|
|
9542
9471
|
* that are already carrying real people is a person's call, so cancel takes
|
|
@@ -9575,7 +9504,7 @@ const executions = defineModule(defineRoute({
|
|
|
9575
9504
|
tags: ["executions"],
|
|
9576
9505
|
summary: "Inspect one execution, with its logs",
|
|
9577
9506
|
security: PIPELINE_AUTH,
|
|
9578
|
-
request: { params: params$
|
|
9507
|
+
request: { params: params$8 },
|
|
9579
9508
|
responses: {
|
|
9580
9509
|
200: envelope(executionDetailSchema),
|
|
9581
9510
|
...sessionErrors,
|
|
@@ -9688,16 +9617,11 @@ const ingestion = defineModule(defineRoute({
|
|
|
9688
9617
|
* because the rows are derived from a push rather than created through the
|
|
9689
9618
|
* API, and (org, key) is the only identity they have.
|
|
9690
9619
|
*
|
|
9691
|
-
* `list` and `get` report the enabled flag of BOTH environments whichever
|
|
9692
|
-
* one the caller selected, because "is this on in production?" is the
|
|
9693
|
-
* question they exist to answer; `enable` and `disable` act on the selected
|
|
9694
|
-
* one, like every other environment-scoped write (docs/standards/api.md).
|
|
9695
|
-
*
|
|
9696
9620
|
* `journeys.listInstances` and `journeys.getInstance` are gone: executions
|
|
9697
9621
|
* are their own resource now (`executions.list` / `executions.get`), and
|
|
9698
9622
|
* `journeys.dryRun` answers with the execution it started.
|
|
9699
9623
|
*/
|
|
9700
|
-
const params$
|
|
9624
|
+
const params$7 = z.object({ key: z.string() });
|
|
9701
9625
|
const journeys = defineModule(defineRoute({
|
|
9702
9626
|
method: "get",
|
|
9703
9627
|
path: "/v1/journeys",
|
|
@@ -9718,7 +9642,7 @@ const journeys = defineModule(defineRoute({
|
|
|
9718
9642
|
tags: ["journeys"],
|
|
9719
9643
|
summary: "Inspect one journey",
|
|
9720
9644
|
security: SESSION_AUTH,
|
|
9721
|
-
request: { params: params$
|
|
9645
|
+
request: { params: params$7 },
|
|
9722
9646
|
responses: {
|
|
9723
9647
|
200: envelope(journeySchema),
|
|
9724
9648
|
...sessionErrors,
|
|
@@ -9731,7 +9655,7 @@ const journeys = defineModule(defineRoute({
|
|
|
9731
9655
|
tags: ["journeys"],
|
|
9732
9656
|
summary: "Journey operational stats",
|
|
9733
9657
|
security: SESSION_AUTH,
|
|
9734
|
-
request: { params: params$
|
|
9658
|
+
request: { params: params$7 },
|
|
9735
9659
|
responses: {
|
|
9736
9660
|
200: envelope(journeyStatsSchema),
|
|
9737
9661
|
...sessionErrors,
|
|
@@ -9742,7 +9666,7 @@ const journeys = defineModule(defineRoute({
|
|
|
9742
9666
|
path: "/v1/journeys/enable",
|
|
9743
9667
|
operationId: "journeys.enable",
|
|
9744
9668
|
tags: ["journeys"],
|
|
9745
|
-
summary: "Turn journeys on
|
|
9669
|
+
summary: "Turn journeys on",
|
|
9746
9670
|
security: PIPELINE_AUTH,
|
|
9747
9671
|
surfaces: { cli: false },
|
|
9748
9672
|
request: { body: jsonBody(setJourneysEnabledBodySchema) },
|
|
@@ -9756,7 +9680,7 @@ const journeys = defineModule(defineRoute({
|
|
|
9756
9680
|
path: "/v1/journeys/disable",
|
|
9757
9681
|
operationId: "journeys.disable",
|
|
9758
9682
|
tags: ["journeys"],
|
|
9759
|
-
summary: "Turn journeys off
|
|
9683
|
+
summary: "Turn journeys off",
|
|
9760
9684
|
security: PIPELINE_AUTH,
|
|
9761
9685
|
surfaces: { cli: false },
|
|
9762
9686
|
request: { body: jsonBody(setJourneysEnabledBodySchema) },
|
|
@@ -9772,7 +9696,7 @@ const journeys = defineModule(defineRoute({
|
|
|
9772
9696
|
tags: ["journeys"],
|
|
9773
9697
|
summary: "Delete a journey",
|
|
9774
9698
|
security: SESSION_AUTH,
|
|
9775
|
-
request: { params: params$
|
|
9699
|
+
request: { params: params$7 },
|
|
9776
9700
|
responses: {
|
|
9777
9701
|
200: envelope(deletedSchema),
|
|
9778
9702
|
...sessionErrors,
|
|
@@ -9786,7 +9710,7 @@ const journeys = defineModule(defineRoute({
|
|
|
9786
9710
|
summary: "Dry-run a journey against a real user",
|
|
9787
9711
|
security: SESSION_AUTH,
|
|
9788
9712
|
request: {
|
|
9789
|
-
params: params$
|
|
9713
|
+
params: params$7,
|
|
9790
9714
|
body: jsonBody(dryRunJourneyBodySchema)
|
|
9791
9715
|
},
|
|
9792
9716
|
responses: {
|
|
@@ -9800,9 +9724,9 @@ const journeys = defineModule(defineRoute({
|
|
|
9800
9724
|
//#region ../../packages/shared/src/contract/me.ts
|
|
9801
9725
|
/**
|
|
9802
9726
|
* The signed-in developer's own settings. Session auth like the rest of the
|
|
9803
|
-
* dashboard API, but deliberately not org-scoped
|
|
9804
|
-
*
|
|
9805
|
-
*
|
|
9727
|
+
* dashboard API, but deliberately not org-scoped: the subject is the
|
|
9728
|
+
* caller's own platform-workspace profile, taken from their session, never
|
|
9729
|
+
* from the request.
|
|
9806
9730
|
*
|
|
9807
9731
|
* Hidden from the CLI and the MCP server. Both act with an org's key on that
|
|
9808
9732
|
* org's data, and neither has a signed-in human whose mail preferences this
|
|
@@ -9899,7 +9823,7 @@ const project = defineModule(defineRoute({
|
|
|
9899
9823
|
|
|
9900
9824
|
//#endregion
|
|
9901
9825
|
//#region ../../packages/shared/src/contract/pushes.ts
|
|
9902
|
-
const params$
|
|
9826
|
+
const params$6 = z.object({ id: z.string() });
|
|
9903
9827
|
/**
|
|
9904
9828
|
* Pushes: one `cow push` each, the whole project at one point in time. The
|
|
9905
9829
|
* push stores the source archive and its compile creates a version of every
|
|
@@ -9944,7 +9868,7 @@ const pushes = defineModule(defineRoute({
|
|
|
9944
9868
|
tags: ["pushes"],
|
|
9945
9869
|
summary: "Inspect one push and what it compiled",
|
|
9946
9870
|
security: SESSION_AUTH,
|
|
9947
|
-
request: { params: params$
|
|
9871
|
+
request: { params: params$6 },
|
|
9948
9872
|
responses: {
|
|
9949
9873
|
200: envelope(pushSchema),
|
|
9950
9874
|
...sessionErrors,
|
|
@@ -9958,7 +9882,7 @@ const pushes = defineModule(defineRoute({
|
|
|
9958
9882
|
summary: "Download the pushed source tarball",
|
|
9959
9883
|
security: SESSION_AUTH,
|
|
9960
9884
|
surfaces: HIDDEN_FROM_TOOLS,
|
|
9961
|
-
request: { params: params$
|
|
9885
|
+
request: { params: params$6 },
|
|
9962
9886
|
responses: {
|
|
9963
9887
|
200: {
|
|
9964
9888
|
description: "The gzipped source tarball the push stored",
|
|
@@ -9969,6 +9893,98 @@ const pushes = defineModule(defineRoute({
|
|
|
9969
9893
|
}
|
|
9970
9894
|
}));
|
|
9971
9895
|
|
|
9896
|
+
//#endregion
|
|
9897
|
+
//#region ../../packages/shared/src/contract/resend.ts
|
|
9898
|
+
/**
|
|
9899
|
+
* The Resend-compatible send facade (ADR 0014): a developer points
|
|
9900
|
+
* `RESEND_BASE_URL` at `/resend/{sourceId}` and keeps their code. The source
|
|
9901
|
+
* in the path names the app, because the Resend SDK carries no other field
|
|
9902
|
+
* and accepts no custom header; the bearer is the organization's ingestion
|
|
9903
|
+
* key.
|
|
9904
|
+
*
|
|
9905
|
+
* These routes are the one exception to the envelope standard: they answer
|
|
9906
|
+
* Resend's own shapes, and the API mounts them on a sub-app with its own
|
|
9907
|
+
* error handler. They stay in the contract so the API still registers no
|
|
9908
|
+
* route outside it, and they are hidden from the CLI, the MCP server and the
|
|
9909
|
+
* generated reference: a Cowliss surface has the `/v1` routes, and the guide
|
|
9910
|
+
* is the place the facade is documented.
|
|
9911
|
+
*/
|
|
9912
|
+
const FACADE_SURFACES = {
|
|
9913
|
+
cli: false,
|
|
9914
|
+
mcp: false,
|
|
9915
|
+
docs: false
|
|
9916
|
+
};
|
|
9917
|
+
const sourceParam = z.object({ sourceId: z.string().openapi({ example: "src_2f9a8c1b" }) });
|
|
9918
|
+
/** Resend's error body, for the answers this facade gives. */
|
|
9919
|
+
const facadeError = (description) => ({
|
|
9920
|
+
description,
|
|
9921
|
+
content: { "application/json": { schema: resendErrorSchema } }
|
|
9922
|
+
});
|
|
9923
|
+
const facadeErrors = {
|
|
9924
|
+
401: facadeError("missing_api_key | invalid_api_key"),
|
|
9925
|
+
404: facadeError("not_found: unknown source, or unknown message"),
|
|
9926
|
+
422: facadeError("validation_error | missing_required_field"),
|
|
9927
|
+
429: facadeError("monthly_quota_exceeded | rate_limit_exceeded"),
|
|
9928
|
+
500: facadeError("application_error")
|
|
9929
|
+
};
|
|
9930
|
+
const resend = defineModule(defineRoute({
|
|
9931
|
+
method: "post",
|
|
9932
|
+
path: "/resend/{sourceId}/emails",
|
|
9933
|
+
operationId: "resend.send",
|
|
9934
|
+
tags: ["resend"],
|
|
9935
|
+
summary: "Send one email (Resend-compatible)",
|
|
9936
|
+
security: API_KEY_AUTH,
|
|
9937
|
+
surfaces: FACADE_SURFACES,
|
|
9938
|
+
request: {
|
|
9939
|
+
params: sourceParam,
|
|
9940
|
+
body: jsonBody(resendSendBodySchema)
|
|
9941
|
+
},
|
|
9942
|
+
responses: {
|
|
9943
|
+
201: {
|
|
9944
|
+
description: "Accepted; the id is the delivery's",
|
|
9945
|
+
content: { "application/json": { schema: resendSentSchema } }
|
|
9946
|
+
},
|
|
9947
|
+
403: facadeError("validation_error: the from-address sits on a domain this organization has not verified"),
|
|
9948
|
+
...facadeErrors
|
|
9949
|
+
}
|
|
9950
|
+
}), defineRoute({
|
|
9951
|
+
method: "post",
|
|
9952
|
+
path: "/resend/{sourceId}/emails/batch",
|
|
9953
|
+
operationId: "resend.sendBatch",
|
|
9954
|
+
tags: ["resend"],
|
|
9955
|
+
summary: "Send up to 100 emails (Resend-compatible)",
|
|
9956
|
+
security: API_KEY_AUTH,
|
|
9957
|
+
surfaces: FACADE_SURFACES,
|
|
9958
|
+
request: {
|
|
9959
|
+
params: sourceParam,
|
|
9960
|
+
body: jsonBody(resendBatchBodySchema)
|
|
9961
|
+
},
|
|
9962
|
+
responses: {
|
|
9963
|
+
201: {
|
|
9964
|
+
description: "Accepted; one id per message, in the order they were given",
|
|
9965
|
+
content: { "application/json": { schema: resendBatchSentSchema } }
|
|
9966
|
+
},
|
|
9967
|
+
403: facadeError("validation_error: a from-address sits on a domain this organization has not verified"),
|
|
9968
|
+
...facadeErrors
|
|
9969
|
+
}
|
|
9970
|
+
}), defineRoute({
|
|
9971
|
+
method: "get",
|
|
9972
|
+
path: "/resend/{sourceId}/emails/{id}",
|
|
9973
|
+
operationId: "resend.get",
|
|
9974
|
+
tags: ["resend"],
|
|
9975
|
+
summary: "Fetch one sent email (Resend-compatible)",
|
|
9976
|
+
security: API_KEY_AUTH,
|
|
9977
|
+
surfaces: FACADE_SURFACES,
|
|
9978
|
+
request: { params: sourceParam.extend({ id: z.string().openapi({ example: "dlv_2f9a8c1b" }) }) },
|
|
9979
|
+
responses: {
|
|
9980
|
+
200: {
|
|
9981
|
+
description: "The email object",
|
|
9982
|
+
content: { "application/json": { schema: resendEmailSchema } }
|
|
9983
|
+
},
|
|
9984
|
+
...facadeErrors
|
|
9985
|
+
}
|
|
9986
|
+
}));
|
|
9987
|
+
|
|
9972
9988
|
//#endregion
|
|
9973
9989
|
//#region ../../packages/shared/src/contract/review.ts
|
|
9974
9990
|
/**
|
|
@@ -9994,7 +10010,7 @@ const review = defineModule(defineRoute({
|
|
|
9994
10010
|
|
|
9995
10011
|
//#endregion
|
|
9996
10012
|
//#region ../../packages/shared/src/contract/segments.ts
|
|
9997
|
-
const params$
|
|
10013
|
+
const params$5 = z.object({ id: z.string() });
|
|
9998
10014
|
const writeErrors = errors("validation_failed", "malformed_request");
|
|
9999
10015
|
/** `preview` precedes `get` so "preview" is never read as an id. */
|
|
10000
10016
|
const segments = defineModule(defineRoute({
|
|
@@ -10044,7 +10060,7 @@ const segments = defineModule(defineRoute({
|
|
|
10044
10060
|
tags: ["segments"],
|
|
10045
10061
|
summary: "Fetch one segment with its member count",
|
|
10046
10062
|
security: SESSION_AUTH,
|
|
10047
|
-
request: { params: params$
|
|
10063
|
+
request: { params: params$5 },
|
|
10048
10064
|
responses: {
|
|
10049
10065
|
200: envelope(segmentDetailSchema),
|
|
10050
10066
|
...sessionErrors,
|
|
@@ -10058,7 +10074,7 @@ const segments = defineModule(defineRoute({
|
|
|
10058
10074
|
summary: "Update a segment",
|
|
10059
10075
|
security: SESSION_AUTH,
|
|
10060
10076
|
request: {
|
|
10061
|
-
params: params$
|
|
10077
|
+
params: params$5,
|
|
10062
10078
|
body: jsonBody(updateSegmentBodySchema)
|
|
10063
10079
|
},
|
|
10064
10080
|
responses: {
|
|
@@ -10074,7 +10090,7 @@ const segments = defineModule(defineRoute({
|
|
|
10074
10090
|
tags: ["segments"],
|
|
10075
10091
|
summary: "Delete a segment",
|
|
10076
10092
|
security: SESSION_AUTH,
|
|
10077
|
-
request: { params: params$
|
|
10093
|
+
request: { params: params$5 },
|
|
10078
10094
|
responses: {
|
|
10079
10095
|
200: envelope(deletedSchema),
|
|
10080
10096
|
...sessionErrors,
|
|
@@ -10088,7 +10104,7 @@ const segments = defineModule(defineRoute({
|
|
|
10088
10104
|
summary: "List a segment's members",
|
|
10089
10105
|
security: SESSION_AUTH,
|
|
10090
10106
|
request: {
|
|
10091
|
-
params: params$
|
|
10107
|
+
params: params$5,
|
|
10092
10108
|
query: listSegmentMembersQuerySchema
|
|
10093
10109
|
},
|
|
10094
10110
|
responses: {
|
|
@@ -10213,7 +10229,7 @@ const settings = defineModule(defineRoute({
|
|
|
10213
10229
|
//#endregion
|
|
10214
10230
|
//#region ../../packages/shared/src/contract/sources.ts
|
|
10215
10231
|
const appParams = z.object({ appId: z.string() });
|
|
10216
|
-
const params$
|
|
10232
|
+
const params$4 = z.object({ id: z.string() });
|
|
10217
10233
|
/**
|
|
10218
10234
|
* A source is one inbound pipe into an app. It is created and listed under
|
|
10219
10235
|
* its parent app (the app is the attribution unit) and addressed by its own
|
|
@@ -10259,7 +10275,7 @@ const sources = defineModule(defineRoute({
|
|
|
10259
10275
|
tags: ["sources"],
|
|
10260
10276
|
summary: "Fetch one source",
|
|
10261
10277
|
security: SESSION_AUTH,
|
|
10262
|
-
request: { params: params$
|
|
10278
|
+
request: { params: params$4 },
|
|
10263
10279
|
responses: {
|
|
10264
10280
|
200: envelope(sourceSchema),
|
|
10265
10281
|
...sessionErrors,
|
|
@@ -10273,7 +10289,7 @@ const sources = defineModule(defineRoute({
|
|
|
10273
10289
|
summary: "Set or rotate a source's config",
|
|
10274
10290
|
security: SESSION_AUTH,
|
|
10275
10291
|
request: {
|
|
10276
|
-
params: params$
|
|
10292
|
+
params: params$4,
|
|
10277
10293
|
body: jsonBody(updateSourceBodySchema)
|
|
10278
10294
|
},
|
|
10279
10295
|
responses: {
|
|
@@ -10288,7 +10304,7 @@ const sources = defineModule(defineRoute({
|
|
|
10288
10304
|
tags: ["sources"],
|
|
10289
10305
|
summary: "Archive a source",
|
|
10290
10306
|
security: SESSION_AUTH,
|
|
10291
|
-
request: { params: params$
|
|
10307
|
+
request: { params: params$4 },
|
|
10292
10308
|
responses: {
|
|
10293
10309
|
200: envelope(sourceSchema, "The archived source"),
|
|
10294
10310
|
...sessionErrors,
|
|
@@ -10359,7 +10375,7 @@ const suppressions = defineModule(defineRoute({
|
|
|
10359
10375
|
* organization's default sender: it reaches no recipient, so it passes no
|
|
10360
10376
|
* consent gate, counts against nothing, and is logged as a test.
|
|
10361
10377
|
*/
|
|
10362
|
-
const params$
|
|
10378
|
+
const params$3 = z.object({ key: z.string() });
|
|
10363
10379
|
const templates = defineModule(defineRoute({
|
|
10364
10380
|
method: "get",
|
|
10365
10381
|
path: "/v1/templates",
|
|
@@ -10380,7 +10396,7 @@ const templates = defineModule(defineRoute({
|
|
|
10380
10396
|
tags: ["templates"],
|
|
10381
10397
|
summary: "Inspect one email template",
|
|
10382
10398
|
security: SESSION_AUTH,
|
|
10383
|
-
request: { params: params$
|
|
10399
|
+
request: { params: params$3 },
|
|
10384
10400
|
responses: {
|
|
10385
10401
|
200: envelope(templateSchema),
|
|
10386
10402
|
...sessionErrors,
|
|
@@ -10394,7 +10410,7 @@ const templates = defineModule(defineRoute({
|
|
|
10394
10410
|
summary: "Render a template with the props you pass",
|
|
10395
10411
|
security: SESSION_AUTH,
|
|
10396
10412
|
request: {
|
|
10397
|
-
params: params$
|
|
10413
|
+
params: params$3,
|
|
10398
10414
|
body: jsonBody(renderTemplateBodySchema)
|
|
10399
10415
|
},
|
|
10400
10416
|
responses: {
|
|
@@ -10410,8 +10426,8 @@ const templates = defineModule(defineRoute({
|
|
|
10410
10426
|
summary: "Send a rendered template to your own address",
|
|
10411
10427
|
security: SESSION_AUTH,
|
|
10412
10428
|
request: {
|
|
10413
|
-
params: params$
|
|
10414
|
-
body: jsonBody(
|
|
10429
|
+
params: params$3,
|
|
10430
|
+
body: jsonBody(testSendTemplateBodySchema)
|
|
10415
10431
|
},
|
|
10416
10432
|
responses: {
|
|
10417
10433
|
201: envelope(deliverySchema, "The test send, as it was logged"),
|
|
@@ -10425,7 +10441,7 @@ const templates = defineModule(defineRoute({
|
|
|
10425
10441
|
tags: ["templates"],
|
|
10426
10442
|
summary: "Delete an email template",
|
|
10427
10443
|
security: SESSION_AUTH,
|
|
10428
|
-
request: { params: params$
|
|
10444
|
+
request: { params: params$3 },
|
|
10429
10445
|
responses: {
|
|
10430
10446
|
200: envelope(deletedSchema),
|
|
10431
10447
|
...sessionErrors,
|
|
@@ -10435,7 +10451,7 @@ const templates = defineModule(defineRoute({
|
|
|
10435
10451
|
|
|
10436
10452
|
//#endregion
|
|
10437
10453
|
//#region ../../packages/shared/src/contract/users.ts
|
|
10438
|
-
const params$
|
|
10454
|
+
const params$2 = z.object({ profileId: z.string() });
|
|
10439
10455
|
/**
|
|
10440
10456
|
* The answer a merged-away profile id gets (spec: Identity): 307 to the
|
|
10441
10457
|
* same path with the survivor's id, so the method and body of a redirected
|
|
@@ -10446,9 +10462,8 @@ const mergedRedirect = { 307: {
|
|
|
10446
10462
|
headers: z.object({ Location: z.string().meta({ description: "The survivor's URL" }) })
|
|
10447
10463
|
} };
|
|
10448
10464
|
/**
|
|
10449
|
-
* Profiles by their Cowliss-generated id
|
|
10450
|
-
*
|
|
10451
|
-
* wins.
|
|
10465
|
+
* Profiles by their Cowliss-generated id. `find` precedes `{profileId}` so
|
|
10466
|
+
* the literal path wins.
|
|
10452
10467
|
*/
|
|
10453
10468
|
const users = defineModule(defineRoute({
|
|
10454
10469
|
method: "get",
|
|
@@ -10483,7 +10498,7 @@ const users = defineModule(defineRoute({
|
|
|
10483
10498
|
tags: ["users"],
|
|
10484
10499
|
summary: "Fetch one profile with its identifiers",
|
|
10485
10500
|
security: SESSION_AUTH,
|
|
10486
|
-
request: { params: params$
|
|
10501
|
+
request: { params: params$2 },
|
|
10487
10502
|
responses: {
|
|
10488
10503
|
200: envelope(userDetailSchema),
|
|
10489
10504
|
...mergedRedirect,
|
|
@@ -10497,7 +10512,7 @@ const users = defineModule(defineRoute({
|
|
|
10497
10512
|
tags: ["users"],
|
|
10498
10513
|
summary: "Erase a user (GDPR)",
|
|
10499
10514
|
security: SESSION_AUTH,
|
|
10500
|
-
request: { params: params$
|
|
10515
|
+
request: { params: params$2 },
|
|
10501
10516
|
responses: {
|
|
10502
10517
|
200: envelope(eraseUserResponseSchema),
|
|
10503
10518
|
...mergedRedirect,
|
|
@@ -10511,7 +10526,7 @@ const users = defineModule(defineRoute({
|
|
|
10511
10526
|
tags: ["users"],
|
|
10512
10527
|
summary: "Export everything held on a user (GDPR)",
|
|
10513
10528
|
security: SESSION_AUTH,
|
|
10514
|
-
request: { params: params$
|
|
10529
|
+
request: { params: params$2 },
|
|
10515
10530
|
responses: {
|
|
10516
10531
|
200: envelope(userExportSchema),
|
|
10517
10532
|
...mergedRedirect,
|
|
@@ -10526,7 +10541,7 @@ const users = defineModule(defineRoute({
|
|
|
10526
10541
|
summary: "Update consent purposes",
|
|
10527
10542
|
security: SESSION_AUTH,
|
|
10528
10543
|
request: {
|
|
10529
|
-
params: params$
|
|
10544
|
+
params: params$2,
|
|
10530
10545
|
body: jsonBody(updateConsentBodySchema)
|
|
10531
10546
|
},
|
|
10532
10547
|
responses: {
|
|
@@ -10543,7 +10558,7 @@ const users = defineModule(defineRoute({
|
|
|
10543
10558
|
summary: "The segments a profile belongs to",
|
|
10544
10559
|
security: SESSION_AUTH,
|
|
10545
10560
|
request: {
|
|
10546
|
-
params: params$
|
|
10561
|
+
params: params$2,
|
|
10547
10562
|
query: listUserSegmentsQuerySchema
|
|
10548
10563
|
},
|
|
10549
10564
|
responses: {
|
|
@@ -10552,23 +10567,6 @@ const users = defineModule(defineRoute({
|
|
|
10552
10567
|
...sessionErrors,
|
|
10553
10568
|
...errors("not_found", "validation_failed")
|
|
10554
10569
|
}
|
|
10555
|
-
}), defineRoute({
|
|
10556
|
-
method: "get",
|
|
10557
|
-
path: "/v1/users/{profileId}/journeys",
|
|
10558
|
-
operationId: "users.listJourneys",
|
|
10559
|
-
tags: ["users"],
|
|
10560
|
-
summary: "A profile's journey timeline",
|
|
10561
|
-
security: SESSION_AUTH,
|
|
10562
|
-
request: {
|
|
10563
|
-
params: params$1,
|
|
10564
|
-
query: listUserJourneysQuerySchema
|
|
10565
|
-
},
|
|
10566
|
-
responses: {
|
|
10567
|
-
200: list(userJourneySchema),
|
|
10568
|
-
...mergedRedirect,
|
|
10569
|
-
...sessionErrors,
|
|
10570
|
-
...errors("not_found", "validation_failed")
|
|
10571
|
-
}
|
|
10572
10570
|
}));
|
|
10573
10571
|
|
|
10574
10572
|
//#endregion
|
|
@@ -10596,7 +10594,7 @@ const versions = defineModule(defineRoute({
|
|
|
10596
10594
|
|
|
10597
10595
|
//#endregion
|
|
10598
10596
|
//#region ../../packages/shared/src/contract/violations.ts
|
|
10599
|
-
const params = z.object({ id: z.string() });
|
|
10597
|
+
const params$1 = z.object({ id: z.string() });
|
|
10600
10598
|
const violations = defineModule(defineRoute({
|
|
10601
10599
|
method: "get",
|
|
10602
10600
|
path: "/v1/violations/{id}",
|
|
@@ -10604,7 +10602,7 @@ const violations = defineModule(defineRoute({
|
|
|
10604
10602
|
tags: ["violations"],
|
|
10605
10603
|
summary: "Fetch one violation",
|
|
10606
10604
|
security: SESSION_AUTH,
|
|
10607
|
-
request: { params },
|
|
10605
|
+
request: { params: params$1 },
|
|
10608
10606
|
responses: {
|
|
10609
10607
|
200: envelope(violationSchema),
|
|
10610
10608
|
...sessionErrors,
|
|
@@ -10618,7 +10616,7 @@ const violations = defineModule(defineRoute({
|
|
|
10618
10616
|
summary: "Resolve a violation into the catalog",
|
|
10619
10617
|
security: SESSION_AUTH,
|
|
10620
10618
|
request: {
|
|
10621
|
-
params,
|
|
10619
|
+
params: params$1,
|
|
10622
10620
|
body: jsonBody(resolveViolationBodySchema)
|
|
10623
10621
|
},
|
|
10624
10622
|
responses: {
|
|
@@ -10633,7 +10631,7 @@ const violations = defineModule(defineRoute({
|
|
|
10633
10631
|
tags: ["violations"],
|
|
10634
10632
|
summary: "Dismiss a violation",
|
|
10635
10633
|
security: SESSION_AUTH,
|
|
10636
|
-
request: { params },
|
|
10634
|
+
request: { params: params$1 },
|
|
10637
10635
|
responses: {
|
|
10638
10636
|
200: envelope(violationSchema),
|
|
10639
10637
|
...sessionErrors,
|
|
@@ -10641,6 +10639,79 @@ const violations = defineModule(defineRoute({
|
|
|
10641
10639
|
}
|
|
10642
10640
|
}));
|
|
10643
10641
|
|
|
10642
|
+
//#endregion
|
|
10643
|
+
//#region ../../packages/shared/src/contract/webhooks.ts
|
|
10644
|
+
const params = z.object({ id: z.string() });
|
|
10645
|
+
const webhooks = defineModule(defineRoute({
|
|
10646
|
+
method: "post",
|
|
10647
|
+
path: "/v1/webhooks",
|
|
10648
|
+
operationId: "webhooks.create",
|
|
10649
|
+
tags: ["webhooks"],
|
|
10650
|
+
summary: "Create a webhook",
|
|
10651
|
+
security: SESSION_AUTH,
|
|
10652
|
+
request: { body: jsonBody(createWebhookBodySchema) },
|
|
10653
|
+
responses: {
|
|
10654
|
+
201: envelope(webhookCreatedSchema, "The created webhook; the only response carrying the signing secret"),
|
|
10655
|
+
...sessionErrors,
|
|
10656
|
+
...errors("validation_failed", "malformed_request", "conflict")
|
|
10657
|
+
}
|
|
10658
|
+
}), defineRoute({
|
|
10659
|
+
method: "get",
|
|
10660
|
+
path: "/v1/webhooks",
|
|
10661
|
+
operationId: "webhooks.list",
|
|
10662
|
+
tags: ["webhooks"],
|
|
10663
|
+
summary: "List webhooks",
|
|
10664
|
+
security: SESSION_AUTH,
|
|
10665
|
+
request: { query: listWebhooksQuerySchema },
|
|
10666
|
+
responses: {
|
|
10667
|
+
200: list(webhookSchema),
|
|
10668
|
+
...sessionErrors,
|
|
10669
|
+
...errors("validation_failed")
|
|
10670
|
+
}
|
|
10671
|
+
}), defineRoute({
|
|
10672
|
+
method: "get",
|
|
10673
|
+
path: "/v1/webhooks/{id}",
|
|
10674
|
+
operationId: "webhooks.get",
|
|
10675
|
+
tags: ["webhooks"],
|
|
10676
|
+
summary: "Fetch one webhook",
|
|
10677
|
+
security: SESSION_AUTH,
|
|
10678
|
+
request: { params },
|
|
10679
|
+
responses: {
|
|
10680
|
+
200: envelope(webhookSchema),
|
|
10681
|
+
...sessionErrors,
|
|
10682
|
+
...errors("not_found")
|
|
10683
|
+
}
|
|
10684
|
+
}), defineRoute({
|
|
10685
|
+
method: "patch",
|
|
10686
|
+
path: "/v1/webhooks/{id}",
|
|
10687
|
+
operationId: "webhooks.update",
|
|
10688
|
+
tags: ["webhooks"],
|
|
10689
|
+
summary: "Update a webhook",
|
|
10690
|
+
security: SESSION_AUTH,
|
|
10691
|
+
request: {
|
|
10692
|
+
params,
|
|
10693
|
+
body: jsonBody(updateWebhookBodySchema)
|
|
10694
|
+
},
|
|
10695
|
+
responses: {
|
|
10696
|
+
200: envelope(webhookSchema),
|
|
10697
|
+
...sessionErrors,
|
|
10698
|
+
...errors("not_found", "validation_failed", "malformed_request", "conflict")
|
|
10699
|
+
}
|
|
10700
|
+
}), defineRoute({
|
|
10701
|
+
method: "delete",
|
|
10702
|
+
path: "/v1/webhooks/{id}",
|
|
10703
|
+
operationId: "webhooks.delete",
|
|
10704
|
+
tags: ["webhooks"],
|
|
10705
|
+
summary: "Delete a webhook",
|
|
10706
|
+
security: SESSION_AUTH,
|
|
10707
|
+
request: { params },
|
|
10708
|
+
responses: {
|
|
10709
|
+
200: envelope(deletedSchema),
|
|
10710
|
+
...sessionErrors,
|
|
10711
|
+
...errors("not_found")
|
|
10712
|
+
}
|
|
10713
|
+
}));
|
|
10714
|
+
|
|
10644
10715
|
//#endregion
|
|
10645
10716
|
//#region ../../packages/shared/src/contract/index.ts
|
|
10646
10717
|
/**
|
|
@@ -10673,10 +10744,11 @@ const contract = {
|
|
|
10673
10744
|
executions,
|
|
10674
10745
|
journeys,
|
|
10675
10746
|
templates,
|
|
10676
|
-
|
|
10747
|
+
webhooks,
|
|
10677
10748
|
domains,
|
|
10678
10749
|
deliveries,
|
|
10679
10750
|
emails,
|
|
10751
|
+
resend,
|
|
10680
10752
|
suppressions,
|
|
10681
10753
|
settings,
|
|
10682
10754
|
billing
|
|
@@ -11048,15 +11120,10 @@ function registerContractCommands(program, run) {
|
|
|
11048
11120
|
//#endregion
|
|
11049
11121
|
//#region src/commands/enable.ts
|
|
11050
11122
|
/**
|
|
11051
|
-
* `cow enable` and `cow disable`: the one gate on a journey,
|
|
11052
|
-
*
|
|
11053
|
-
*
|
|
11054
|
-
*
|
|
11055
|
-
* same set of keys they do.
|
|
11056
|
-
*
|
|
11057
|
-
* `--env` is never defaulted here (`requireEnvironment`): the default
|
|
11058
|
-
* environment is production, and a flag nobody typed must not put code in
|
|
11059
|
-
* front of real recipients.
|
|
11123
|
+
* `cow enable` and `cow disable`: the one gate on a journey (ADR 0011, ADR
|
|
11124
|
+
* 0013). Hand-written rather than derived from `journeys.enable`, because
|
|
11125
|
+
* the commands are top-level names a developer types; the route itself
|
|
11126
|
+
* takes the same set of keys they do.
|
|
11060
11127
|
*/
|
|
11061
11128
|
/**
|
|
11062
11129
|
* One call, whichever way the keys were named: the route takes the set, and
|
|
@@ -11068,33 +11135,32 @@ async function setEnabled(client, selector, enabled) {
|
|
|
11068
11135
|
const route = enabled ? contract.journeys["journeys.enable"] : contract.journeys["journeys.disable"];
|
|
11069
11136
|
return (await client.request(route, { body: selector })).data;
|
|
11070
11137
|
}
|
|
11071
|
-
/** What the terminal says: one line per key, naming
|
|
11072
|
-
function flipSummary(journeys, enabled
|
|
11138
|
+
/** What the terminal says: one line per key, naming its new state. */
|
|
11139
|
+
function flipSummary(journeys, enabled) {
|
|
11073
11140
|
if (journeys.length === 0) return `This project has no journeys to turn ${enabled ? "on" : "off"}.`;
|
|
11074
|
-
return journeys.map((journey) => `${journey.key} is ${enabled ? "on" : "off"}
|
|
11141
|
+
return journeys.map((journey) => `${journey.key} is ${enabled ? "on" : "off"}.`).join("\n");
|
|
11075
11142
|
}
|
|
11076
|
-
function register(program, clientFor,
|
|
11143
|
+
function register(program, clientFor, io, enabled) {
|
|
11077
11144
|
const verb = enabled ? "enable" : "disable";
|
|
11078
|
-
program.command(verb).description(`turn journeys ${enabled ? "on" : "off"}
|
|
11145
|
+
program.command(verb).description(`turn journeys ${enabled ? "on" : "off"} (many keys, or --all for this project's)`).argument("[keys...]", "journey keys").option("--all", "every journey of this project").action(async (keys, opts) => {
|
|
11079
11146
|
const merged = {
|
|
11080
11147
|
...program.opts(),
|
|
11081
11148
|
...opts
|
|
11082
11149
|
};
|
|
11083
|
-
|
|
11084
|
-
if (merged.all
|
|
11085
|
-
if (merged.all !== true && keys.length === 0) throw new Error(`Name at least one journey, or pass --all: cow ${verb} welcome --env ${environment}.`);
|
|
11150
|
+
if (merged.all === true && keys.length > 0) throw new Error(`Name the journeys or pass --all, not both: cow ${verb} welcome.`);
|
|
11151
|
+
if (merged.all !== true && keys.length === 0) throw new Error(`Name at least one journey, or pass --all: cow ${verb} welcome.`);
|
|
11086
11152
|
const selector = merged.all === true ? { project: (await assertCowConfig(process.cwd())).project } : { keys };
|
|
11087
11153
|
const updated = await setEnabled(await clientFor(merged), selector, enabled);
|
|
11088
11154
|
if (merged.json === true) {
|
|
11089
11155
|
emit({ data: updated }, io, true);
|
|
11090
11156
|
return;
|
|
11091
11157
|
}
|
|
11092
|
-
io.stdout(`${flipSummary(updated, enabled
|
|
11158
|
+
io.stdout(`${flipSummary(updated, enabled)}\n`);
|
|
11093
11159
|
});
|
|
11094
11160
|
}
|
|
11095
|
-
function registerEnable(program, clientFor,
|
|
11096
|
-
register(program, clientFor,
|
|
11097
|
-
register(program, clientFor,
|
|
11161
|
+
function registerEnable(program, clientFor, io) {
|
|
11162
|
+
register(program, clientFor, io, true);
|
|
11163
|
+
register(program, clientFor, io, false);
|
|
11098
11164
|
}
|
|
11099
11165
|
|
|
11100
11166
|
//#endregion
|
|
@@ -11150,7 +11216,7 @@ async function packageJson(name) {
|
|
|
11150
11216
|
* ponytail: plain fetch because `POST /v1/project` is not a contract route
|
|
11151
11217
|
* yet; swap it for the typed client's `project.create` once ticket 04 lands.
|
|
11152
11218
|
*/
|
|
11153
|
-
async function createProject(apiUrl, token,
|
|
11219
|
+
async function createProject(apiUrl, token, name) {
|
|
11154
11220
|
if (!token) return {
|
|
11155
11221
|
project: "skipped",
|
|
11156
11222
|
reason: "not logged in"
|
|
@@ -11160,8 +11226,7 @@ async function createProject(apiUrl, token, environment, name) {
|
|
|
11160
11226
|
method: "POST",
|
|
11161
11227
|
headers: {
|
|
11162
11228
|
"content-type": "application/json",
|
|
11163
|
-
authorization: `Bearer ${token}
|
|
11164
|
-
[ENVIRONMENT_HEADER]: environment
|
|
11229
|
+
authorization: `Bearer ${token}`
|
|
11165
11230
|
},
|
|
11166
11231
|
body: JSON.stringify({ data: { name } })
|
|
11167
11232
|
});
|
|
@@ -11220,7 +11285,7 @@ function registerInit(program, env, io) {
|
|
|
11220
11285
|
}
|
|
11221
11286
|
const example = typeof opts.example === "string" ? opts.example : void 0;
|
|
11222
11287
|
const files = [...Object.keys(contents), ...example ? await copyExample(example, dir, false) : []].sort();
|
|
11223
|
-
const outcome = await createProject(resolveApiUrl(env, credentials, typeof merged.api === "string" ? merged.api : void 0), resolveCredential(env, credentials).token,
|
|
11288
|
+
const outcome = await createProject(resolveApiUrl(env, credentials, typeof merged.api === "string" ? merged.api : void 0), resolveCredential(env, credentials).token, name);
|
|
11224
11289
|
emit({ data: {
|
|
11225
11290
|
dir,
|
|
11226
11291
|
orgId,
|
|
@@ -11535,7 +11600,7 @@ async function pushProject({ client, projectDir }) {
|
|
|
11535
11600
|
push: (await client.request(contract.pushes["pushes.create"], { body: {
|
|
11536
11601
|
manifest: {
|
|
11537
11602
|
...manifest,
|
|
11538
|
-
protocol:
|
|
11603
|
+
protocol: 3
|
|
11539
11604
|
},
|
|
11540
11605
|
project: project.name
|
|
11541
11606
|
} })).data,
|
|
@@ -11634,13 +11699,11 @@ function versionPhrase(entry, now) {
|
|
|
11634
11699
|
return `${bundle ? `version ${shortDigest(bundle)}` : "version"}, pushed ${ago(version.pushedAt, now)}${state}`;
|
|
11635
11700
|
}
|
|
11636
11701
|
function flagsPhrase(entry) {
|
|
11637
|
-
if (
|
|
11638
|
-
|
|
11639
|
-
return `${word(entry.enabled.development)} in development, ${word(entry.enabled.production)} in production`;
|
|
11702
|
+
if (entry.enabled === null) return "";
|
|
11703
|
+
return entry.enabled ? "on" : "off";
|
|
11640
11704
|
}
|
|
11641
11705
|
function livePhrase(entry) {
|
|
11642
|
-
|
|
11643
|
-
return [...running(entry.liveExecutions.development, "development"), ...running(entry.liveExecutions.production, "production")].join(", ");
|
|
11706
|
+
return entry.liveExecutions === 0 ? "" : `${entry.liveExecutions} running`;
|
|
11644
11707
|
}
|
|
11645
11708
|
function pad(text, width) {
|
|
11646
11709
|
return text.padEnd(width);
|
|
@@ -11690,11 +11753,9 @@ function statusLines(status, manifest, now) {
|
|
|
11690
11753
|
lines.push("", "On the server and not in this tree:");
|
|
11691
11754
|
for (const entry of missing) lines.push(` ${entry.key}, a ${entry.kind}. Delete it with cow ${entry.kind}s delete ${entry.key}.`);
|
|
11692
11755
|
}
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
|
|
11696
|
-
lines.push("", `Names ${environment} does not define yet:`);
|
|
11697
|
-
for (const warning of warnings) lines.push(` ${warning}`);
|
|
11756
|
+
if (status.warnings.length > 0) {
|
|
11757
|
+
lines.push("", "Names this organization does not define yet:");
|
|
11758
|
+
for (const warning of status.warnings) lines.push(` ${warning}`);
|
|
11698
11759
|
}
|
|
11699
11760
|
return lines;
|
|
11700
11761
|
}
|
|
@@ -11889,7 +11950,7 @@ async function runScenario(projectDir, key, scenario) {
|
|
|
11889
11950
|
let error;
|
|
11890
11951
|
for (;;) {
|
|
11891
11952
|
const output = await runGuest(module, {
|
|
11892
|
-
protocol:
|
|
11953
|
+
protocol: 3,
|
|
11893
11954
|
kind: "journey",
|
|
11894
11955
|
key,
|
|
11895
11956
|
event: trigger,
|
|
@@ -11972,32 +12033,6 @@ function registerTest(program, io) {
|
|
|
11972
12033
|
|
|
11973
12034
|
//#endregion
|
|
11974
12035
|
//#region src/commands/index.ts
|
|
11975
|
-
/**
|
|
11976
|
-
* The environment a call acts on: the root `--env`, else `COW_ENVIRONMENT`,
|
|
11977
|
-
* else production. The client sets it as the selection header; a command
|
|
11978
|
-
* that also needs the value itself reads it from here, so the two can never
|
|
11979
|
-
* disagree.
|
|
11980
|
-
*/
|
|
11981
|
-
function environmentFor(env, opts) {
|
|
11982
|
-
const flag = environmentSchema.safeParse(opts.env);
|
|
11983
|
-
return flag.success ? flag.data : env.COW_ENVIRONMENT ?? "production";
|
|
11984
|
-
}
|
|
11985
|
-
/**
|
|
11986
|
-
* The same value, but only when the caller actually chose one. `cow enable`
|
|
11987
|
-
* and `cow disable` change what real recipients get, and the default is
|
|
11988
|
-
* production: a bare `cow enable welcome` typed while thinking about
|
|
11989
|
-
* development would turn it on in production. A pipeline is unaffected,
|
|
11990
|
-
* because `COW_ENVIRONMENT` counts as having chosen.
|
|
11991
|
-
*/
|
|
11992
|
-
function requireEnvironment(env, opts) {
|
|
11993
|
-
if (opts.env === void 0 && env.COW_ENVIRONMENT === void 0) throw new Error(`Name the environment to act on: --env ${ENVIRONMENTS.join(" or --env ")}, or set COW_ENVIRONMENT.`);
|
|
11994
|
-
return environmentFor(env, opts);
|
|
11995
|
-
}
|
|
11996
|
-
function parseEnvironment(value) {
|
|
11997
|
-
const parsed = environmentSchema.safeParse(value);
|
|
11998
|
-
if (!parsed.success) throw new InvalidArgumentError(`expected one of ${ENVIRONMENTS.join(", ")}`);
|
|
11999
|
-
return parsed.data;
|
|
12000
|
-
}
|
|
12001
12036
|
/** Every command prints the response envelope as JSON. */
|
|
12002
12037
|
function emit(result, io, json) {
|
|
12003
12038
|
const compact = json === true || !io.isTTY;
|
|
@@ -12005,7 +12040,7 @@ function emit(result, io, json) {
|
|
|
12005
12040
|
}
|
|
12006
12041
|
function buildProgram(env, io) {
|
|
12007
12042
|
const program = new Command();
|
|
12008
|
-
program.name("cow").description("Admin CLI for Cowliss: a thin client of the dashboard API").option("--api <url>", "API base URL (overrides env and saved config)").option("--
|
|
12043
|
+
program.name("cow").description("Admin CLI for Cowliss: a thin client of the dashboard API").option("--api <url>", "API base URL (overrides env and saved config)").option("--json", "force compact single-line JSON output").option("--config <file>", `project config to read instead of ${DEFAULT_CONFIG_FILE} (overrides COW_CONFIG)`);
|
|
12009
12044
|
program.hook("preAction", () => {
|
|
12010
12045
|
const flag = program.opts().config;
|
|
12011
12046
|
setConfigFile((typeof flag === "string" ? flag : void 0) ?? env.COW_CONFIG ?? "cow.json");
|
|
@@ -12015,10 +12050,7 @@ function buildProgram(env, io) {
|
|
|
12015
12050
|
return createClient({
|
|
12016
12051
|
baseUrl: resolveApiUrl(env, credentials, typeof opts.api === "string" ? opts.api : void 0, (await readCowConfig(process.cwd()))?.apiUrl),
|
|
12017
12052
|
auth: async () => resolveCredential(env, await readCredentials(env)).token,
|
|
12018
|
-
headers: async () => ({
|
|
12019
|
-
[ENVIRONMENT_HEADER]: environmentFor(env, opts),
|
|
12020
|
-
[CLIENT_HEADER]: `@cowliss/cli/${(await readCliPackage()).version}`
|
|
12021
|
-
})
|
|
12053
|
+
headers: async () => ({ [CLIENT_HEADER]: `@cowliss/cli/${(await readCliPackage()).version}` })
|
|
12022
12054
|
});
|
|
12023
12055
|
};
|
|
12024
12056
|
const run = async (opts, fn) => {
|
|
@@ -12035,7 +12067,7 @@ function buildProgram(env, io) {
|
|
|
12035
12067
|
registerBuild(program, io);
|
|
12036
12068
|
registerPush(program, clientFor, env, io);
|
|
12037
12069
|
registerStatus(program, clientFor, io);
|
|
12038
|
-
registerEnable(program, clientFor,
|
|
12070
|
+
registerEnable(program, clientFor, io);
|
|
12039
12071
|
registerPull(program, clientFor, io);
|
|
12040
12072
|
registerTest(program, io);
|
|
12041
12073
|
registerMcp(program, clientFor, env, io);
|
|
@@ -12056,8 +12088,6 @@ const envSchema = z.object({
|
|
|
12056
12088
|
COW_TOKEN: z.string().min(1).optional(),
|
|
12057
12089
|
/** An org pipeline key for CI; when set it wins over the cached session (ticket 05 reads it). */
|
|
12058
12090
|
COW_PIPELINE_KEY: z.string().min(1).optional(),
|
|
12059
|
-
/** The environment admin calls select; `--env` overrides it, production is the default. */
|
|
12060
|
-
COW_ENVIRONMENT: environmentSchema.optional(),
|
|
12061
12091
|
/**
|
|
12062
12092
|
* The project config to read instead of `cow.json`, so one checkout can
|
|
12063
12093
|
* hold both the committed production config and a local override.
|