@cowliss/cli 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/guest/driver-emails.d.ts +12 -0
- package/dist/guest/{driver-C1bsCjZT.js → driver-emails.js} +11 -312
- package/dist/guest/driver-poSdZIj8.js +320 -0
- package/dist/guest/driver.d.ts +19 -6
- package/dist/guest/driver.js +2 -2
- package/dist/guest/emails-5GindgMQ.d.ts +47 -0
- package/dist/guest/emails.d.ts +1 -46
- package/dist/guest/{index-3PMqJKmc.d.ts → index-EqBCZnpq.d.ts} +11 -12
- package/dist/guest/{journeys-Dpsp225V.js → journeys-F8Yk8s1P.js} +93 -45
- package/dist/guest/journeys.d.ts +13 -8
- package/dist/guest/journeys.js +1 -1
- package/dist/guest/wasi.d.ts +4 -2
- package/dist/guest/wasi.js +1 -3
- package/dist/index.js +546 -354
- package/package.json +17 -1
- package/examples/cross-app-pitch/emails/cross-app-pitch.tsx +0 -77
- package/examples/cross-app-pitch/journeys/cross-app-pitch.ts +0 -39
- package/examples/cross-app-pitch/scenarios/cross-app-pitch.json +0 -17
package/dist/index.js
CHANGED
|
@@ -33,6 +33,34 @@ const DOCS_URL = "https://docs.cowliss.com";
|
|
|
33
33
|
*/
|
|
34
34
|
const REQUEST_ID_HEADER = "X-Request-Id";
|
|
35
35
|
/**
|
|
36
|
+
* Stripe-style ID prefixes per resource type, so ids in logs, URLs, and
|
|
37
|
+
* payloads are self-describing.
|
|
38
|
+
*/
|
|
39
|
+
const ID_PREFIXES = {
|
|
40
|
+
user: "usr_",
|
|
41
|
+
event: "evt_",
|
|
42
|
+
identifier: "idn_",
|
|
43
|
+
segment: "seg_",
|
|
44
|
+
/** The attribution unit: a readable `app_<slug>`, not a TypeID. */
|
|
45
|
+
app: "app_",
|
|
46
|
+
/** One inbound pipe into an app; a TypeID like everything else. */
|
|
47
|
+
source: "src_",
|
|
48
|
+
webhook: "whk_",
|
|
49
|
+
push: "psh_",
|
|
50
|
+
version: "ver_",
|
|
51
|
+
execution: "exe_",
|
|
52
|
+
delivery: "dlv_",
|
|
53
|
+
violation: "vio_",
|
|
54
|
+
quarantineEntry: "qtn_",
|
|
55
|
+
idempotencyKey: "idk_",
|
|
56
|
+
abuseEvent: "abu_",
|
|
57
|
+
suppression: "sup_",
|
|
58
|
+
senderDomain: "dom_",
|
|
59
|
+
journeyRun: "run_",
|
|
60
|
+
topup: "top_",
|
|
61
|
+
emailAddress: "eml_"
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
36
64
|
* Clerk ID prefix marking an organization-scoped subject. Ingestion API keys
|
|
37
65
|
* must resolve to an org subject; user-scoped keys are rejected.
|
|
38
66
|
*/
|
|
@@ -3143,7 +3171,7 @@ const bytea = customType({ dataType: () => "bytea" });
|
|
|
3143
3171
|
/**
|
|
3144
3172
|
* What an artifact is: a journey or template bundle as `cow build` emitted
|
|
3145
3173
|
* it, the wasm module the server compiled from it, or the gzipped source
|
|
3146
|
-
* tarball of the
|
|
3174
|
+
* tarball of the repo the push was built from.
|
|
3147
3175
|
*/
|
|
3148
3176
|
const artifactKindEnum = pgEnum("artifact_kind", [
|
|
3149
3177
|
"bundle",
|
|
@@ -3264,14 +3292,14 @@ const insertCatalogTraitSchema = createInsertSchema(catalogTraits);
|
|
|
3264
3292
|
//#region ../../packages/db/src/schema/consent-purposes.ts
|
|
3265
3293
|
/**
|
|
3266
3294
|
* The consent purposes an org's profiles answer: the two fixed ones, seeded
|
|
3267
|
-
* the first time the set is read, plus whatever the org's
|
|
3268
|
-
*
|
|
3295
|
+
* the first time the set is read, plus whatever the org's apps declare in
|
|
3296
|
+
* their `cow.json`. One read here returns the whole set, so nothing
|
|
3269
3297
|
* downstream unions a table with a constant.
|
|
3270
3298
|
*
|
|
3271
|
-
* Org-wide rather than per-
|
|
3272
|
-
*
|
|
3273
|
-
*
|
|
3274
|
-
*
|
|
3299
|
+
* Org-wide rather than per-app: `profiles.consent` is one jsonb map per
|
|
3300
|
+
* profile, so an answer given under one app is the same answer under the
|
|
3301
|
+
* next, and two apps declaring one key must agree on its label and default
|
|
3302
|
+
* or the deploy is refused.
|
|
3275
3303
|
*
|
|
3276
3304
|
* A deploy adds and updates rows and never deletes one (ADR 0009): profiles
|
|
3277
3305
|
* already hold answers against a purpose, and deleting it would orphan them.
|
|
@@ -3289,14 +3317,18 @@ const consentPurposes = pgTable("consent_purposes", {
|
|
|
3289
3317
|
*/
|
|
3290
3318
|
defaultGranted: boolean("default_granted").notNull(),
|
|
3291
3319
|
/**
|
|
3292
|
-
* The
|
|
3293
|
-
*
|
|
3294
|
-
*
|
|
3320
|
+
* The app whose `cow.json` declared it, and the one a disagreeing deploy
|
|
3321
|
+
* is refused in the name of. Null for the two fixed purposes, which every
|
|
3322
|
+
* org has and no app owns.
|
|
3295
3323
|
*/
|
|
3296
|
-
|
|
3324
|
+
appId: text("app_id"),
|
|
3297
3325
|
createdAt: createdAt(),
|
|
3298
3326
|
updatedAt: updatedAt()
|
|
3299
|
-
}, (table) => [primaryKey({ columns: [table.orgId, table.key] })
|
|
3327
|
+
}, (table) => [primaryKey({ columns: [table.orgId, table.key] }), foreignKey({
|
|
3328
|
+
columns: [table.orgId, table.appId],
|
|
3329
|
+
foreignColumns: [apps$1.orgId, apps$1.id],
|
|
3330
|
+
name: "consent_purposes_app_fk"
|
|
3331
|
+
})]);
|
|
3300
3332
|
|
|
3301
3333
|
//#endregion
|
|
3302
3334
|
//#region ../../packages/db/src/schema/deliveries.ts
|
|
@@ -3392,6 +3424,15 @@ const deliveries$1 = pgTable("deliveries", {
|
|
|
3392
3424
|
* neither of which has one.
|
|
3393
3425
|
*/
|
|
3394
3426
|
journey: text("journey"),
|
|
3427
|
+
/**
|
|
3428
|
+
* The journey execution (`exe_`) this send was made from, so an execution
|
|
3429
|
+
* page can show what it sent. Null is a real state rather than a gap: a
|
|
3430
|
+
* test send and an api send have no execution behind them, and rows
|
|
3431
|
+
* written before this column existed have none to name. No foreign key,
|
|
3432
|
+
* because executions are pruned on their own schedule and a delivery is a
|
|
3433
|
+
* log row that outlives them.
|
|
3434
|
+
*/
|
|
3435
|
+
executionId: text("execution_id"),
|
|
3395
3436
|
kind: deliveryKindEnum("kind").notNull().default("journey"),
|
|
3396
3437
|
/**
|
|
3397
3438
|
* Email template name or webhook name. An api send that named no
|
|
@@ -3449,6 +3490,7 @@ const deliveries$1 = pgTable("deliveries", {
|
|
|
3449
3490
|
}, (table) => [
|
|
3450
3491
|
index("deliveries_org_id_created_at_idx").on(table.orgId, table.createdAt),
|
|
3451
3492
|
index("deliveries_org_id_profile_id_created_at_idx").on(table.orgId, table.profileId, table.createdAt),
|
|
3493
|
+
index("deliveries_org_id_execution_id_created_at_idx").on(table.orgId, table.executionId, table.createdAt),
|
|
3452
3494
|
uniqueIndex("deliveries_org_id_attempt_key_unique").on(table.orgId, table.attemptKey),
|
|
3453
3495
|
uniqueIndex("deliveries_provider_id_unique").on(table.providerId),
|
|
3454
3496
|
index("deliveries_status_sent_at_idx").on(table.status, table.sentAt)
|
|
@@ -3688,10 +3730,10 @@ const identifierKindEnum = pgEnum("identifier_kind", [
|
|
|
3688
3730
|
]);
|
|
3689
3731
|
/**
|
|
3690
3732
|
* The identifiers map: every named identifier a call has ever carried,
|
|
3691
|
-
* pointing at the profile it resolved to. Unique per (org, kind, value),
|
|
3692
|
-
* which is what makes "a shared identifier means the same person"
|
|
3693
|
-
*
|
|
3694
|
-
* is
|
|
3733
|
+
* pointing at the profile it resolved to. Unique per (org, app, kind, value),
|
|
3734
|
+
* which is what makes "a shared identifier means the same person" enforceable
|
|
3735
|
+
* at the row level, inside one app: the same clerkId twice in one app is one
|
|
3736
|
+
* person, and the same email on two apps is two people (ADR 0022).
|
|
3695
3737
|
*
|
|
3696
3738
|
* Rows follow their profile: erasure deletes the profile and these cascade,
|
|
3697
3739
|
* and a merge re-points them at the survivor.
|
|
@@ -3699,11 +3741,21 @@ const identifierKindEnum = pgEnum("identifier_kind", [
|
|
|
3699
3741
|
const identifiers = pgTable("identifiers", {
|
|
3700
3742
|
id: text("id").primaryKey(),
|
|
3701
3743
|
orgId: text("org_id").notNull(),
|
|
3744
|
+
/** The app this identifier resolves in; always its profile's app. */
|
|
3745
|
+
appId: text("app_id").notNull(),
|
|
3702
3746
|
kind: identifierKindEnum("kind").notNull(),
|
|
3703
3747
|
value: text("value").notNull(),
|
|
3704
3748
|
profileId: text("profile_id").notNull().references(() => profiles.id, { onDelete: "cascade" }),
|
|
3705
3749
|
createdAt: createdAt()
|
|
3706
|
-
}, (table) => [
|
|
3750
|
+
}, (table) => [
|
|
3751
|
+
uniqueIndex("identifiers_org_app_kind_value_unique").on(table.orgId, table.appId, table.kind, table.value),
|
|
3752
|
+
index("identifiers_profile_id_idx").on(table.profileId),
|
|
3753
|
+
foreignKey({
|
|
3754
|
+
columns: [table.orgId, table.appId],
|
|
3755
|
+
foreignColumns: [apps$1.orgId, apps$1.id],
|
|
3756
|
+
name: "identifiers_app_fk"
|
|
3757
|
+
})
|
|
3758
|
+
]);
|
|
3707
3759
|
const selectIdentifierSchema = createSelectSchema(identifiers);
|
|
3708
3760
|
const insertIdentifierSchema = createInsertSchema(identifiers);
|
|
3709
3761
|
|
|
@@ -3767,10 +3819,11 @@ const journeyStatusEnum = pgEnum("journey_status", [
|
|
|
3767
3819
|
*
|
|
3768
3820
|
* A push only ever adds and updates: it owns the keys its manifest carries
|
|
3769
3821
|
* and leaves every other row alone, because an org's journeys can come from
|
|
3770
|
-
* several
|
|
3771
|
-
*
|
|
3772
|
-
*
|
|
3773
|
-
*
|
|
3822
|
+
* several apps and a developer may push a repo holding only some of the
|
|
3823
|
+
* files. `appId` records which app put the row here, which is what makes a
|
|
3824
|
+
* key another app owns a refused push rather than a silent overwrite
|
|
3825
|
+
* (ADR 0009, held by the app since ADR 0022). Removing a journey is an
|
|
3826
|
+
* explicit delete.
|
|
3774
3827
|
*
|
|
3775
3828
|
* Whether a journey fires is one field and one only: `status`, which a
|
|
3776
3829
|
* push never writes, so deploying code never turns anything on or off. The
|
|
@@ -3784,14 +3837,14 @@ const journeyStatusEnum = pgEnum("journey_status", [
|
|
|
3784
3837
|
const journeys$1 = pgTable("journeys", {
|
|
3785
3838
|
orgId: text("org_id").notNull(),
|
|
3786
3839
|
key: text("key").notNull(),
|
|
3787
|
-
/** The
|
|
3788
|
-
|
|
3840
|
+
/** The app whose push owns this key, and the only app it fires for. */
|
|
3841
|
+
appId: text("app_id").notNull(),
|
|
3789
3842
|
/** The author's labels, from the manifest; the dashboard's only grouping. */
|
|
3790
3843
|
tags: text("tags").array().notNull().default(sql`'{}'::text[]`),
|
|
3791
3844
|
trigger: jsonb("trigger").$type().notNull(),
|
|
3792
3845
|
/**
|
|
3793
3846
|
* The consent purpose this journey's sends are gated on: one of the two
|
|
3794
|
-
* fixed keys or one the
|
|
3847
|
+
* fixed keys or one the app's repo declares in `cow.json`. Text and not an
|
|
3795
3848
|
* enum, because the set is the org's `consent_purposes` rows, which a
|
|
3796
3849
|
* deploy checks the key against; a database enum could only ever hold
|
|
3797
3850
|
* the fixed pair.
|
|
@@ -3811,7 +3864,11 @@ const journeys$1 = pgTable("journeys", {
|
|
|
3811
3864
|
status: journeyStatusEnum("status").notNull().default("off"),
|
|
3812
3865
|
createdAt: createdAt(),
|
|
3813
3866
|
updatedAt: updatedAt()
|
|
3814
|
-
}, (table) => [primaryKey({ columns: [table.orgId, table.key] })
|
|
3867
|
+
}, (table) => [primaryKey({ columns: [table.orgId, table.key] }), foreignKey({
|
|
3868
|
+
columns: [table.orgId, table.appId],
|
|
3869
|
+
foreignColumns: [apps$1.orgId, apps$1.id],
|
|
3870
|
+
name: "journeys_app_fk"
|
|
3871
|
+
})]);
|
|
3815
3872
|
const selectJourneySchema = createSelectSchema(journeys$1);
|
|
3816
3873
|
const insertJourneySchema = createInsertSchema(journeys$1);
|
|
3817
3874
|
|
|
@@ -3911,33 +3968,15 @@ const orgSettings = pgTable("org_settings", {
|
|
|
3911
3968
|
const selectOrgSettingsSchema = createSelectSchema(orgSettings);
|
|
3912
3969
|
const insertOrgSettingsSchema = createInsertSchema(orgSettings);
|
|
3913
3970
|
|
|
3914
|
-
//#endregion
|
|
3915
|
-
//#region ../../packages/db/src/schema/projects.ts
|
|
3916
|
-
/**
|
|
3917
|
-
* An org's cow project: the folder a developer runs `cow init` in, and the
|
|
3918
|
-
* thing pushes belong to. An org may hold several, one per repo, each with
|
|
3919
|
-
* its own release sequence and its own slice of the deployed journey rows. The name is how `cow.json` picks
|
|
3920
|
-
* one, so it is unique within the org.
|
|
3921
|
-
*/
|
|
3922
|
-
const projects = pgTable("projects", {
|
|
3923
|
-
id: text("id").primaryKey(),
|
|
3924
|
-
orgId: text("org_id").notNull(),
|
|
3925
|
-
name: text("name").notNull(),
|
|
3926
|
-
createdAt: createdAt(),
|
|
3927
|
-
updatedAt: updatedAt()
|
|
3928
|
-
}, (table) => [uniqueIndex("projects_org_id_name_unique").on(table.orgId, table.name)]);
|
|
3929
|
-
const selectProjectSchema = createSelectSchema(projects);
|
|
3930
|
-
const insertProjectSchema = createInsertSchema(projects);
|
|
3931
|
-
|
|
3932
3971
|
//#endregion
|
|
3933
3972
|
//#region ../../packages/db/src/schema/pushes.ts
|
|
3934
3973
|
/**
|
|
3935
|
-
* One `cow push`: the whole
|
|
3974
|
+
* One `cow push`: the whole repo at one point in time. A push stores the
|
|
3936
3975
|
* source archive and creates a version of every key whose bundle changed
|
|
3937
3976
|
* (ADR 0011); it settles nothing and changes no flag, so it has no status
|
|
3938
3977
|
* of its own. What is compiling is read off its versions.
|
|
3939
3978
|
*
|
|
3940
|
-
* `seq` is the human handle and is per
|
|
3979
|
+
* `seq` is the human handle and is per app, allocated inside the create
|
|
3941
3980
|
* transaction; the unique index leads with the org id so it doubles as the
|
|
3942
3981
|
* tenancy key.
|
|
3943
3982
|
*
|
|
@@ -3948,7 +3987,8 @@ const insertProjectSchema = createInsertSchema(projects);
|
|
|
3948
3987
|
const pushes$1 = pgTable("pushes", {
|
|
3949
3988
|
id: text("id").primaryKey(),
|
|
3950
3989
|
orgId: text("org_id").notNull(),
|
|
3951
|
-
|
|
3990
|
+
/** The app this push deployed; the push unit since ADR 0022. */
|
|
3991
|
+
appId: text("app_id").notNull(),
|
|
3952
3992
|
seq: integer("seq").notNull(),
|
|
3953
3993
|
manifest: jsonb("manifest").$type().notNull(),
|
|
3954
3994
|
/** sha256 of the pushed manifest; the CLI compares it to skip a no-op push. */
|
|
@@ -3957,7 +3997,15 @@ const pushes$1 = pgTable("pushes", {
|
|
|
3957
3997
|
sourceDigest: text("source_digest").notNull(),
|
|
3958
3998
|
createdBy: text("created_by").notNull(),
|
|
3959
3999
|
createdAt: createdAt()
|
|
3960
|
-
}, (table) => [
|
|
4000
|
+
}, (table) => [
|
|
4001
|
+
uniqueIndex("pushes_org_id_app_id_seq_unique").on(table.orgId, table.appId, table.seq),
|
|
4002
|
+
index("pushes_org_id_created_at_idx").on(table.orgId, table.createdAt),
|
|
4003
|
+
foreignKey({
|
|
4004
|
+
columns: [table.orgId, table.appId],
|
|
4005
|
+
foreignColumns: [apps$1.orgId, apps$1.id],
|
|
4006
|
+
name: "pushes_app_fk"
|
|
4007
|
+
})
|
|
4008
|
+
]);
|
|
3961
4009
|
const selectPushSchema = createSelectSchema(pushes$1);
|
|
3962
4010
|
const insertPushSchema = createInsertSchema(pushes$1);
|
|
3963
4011
|
/**
|
|
@@ -4062,6 +4110,9 @@ const insertQuarantineEntrySchema = createInsertSchema(quarantineEntries);
|
|
|
4062
4110
|
* hand-maintained: the engine in packages/segments recomputes on every write
|
|
4063
4111
|
* and materializes the result into segment_members.
|
|
4064
4112
|
*
|
|
4113
|
+
* A segment belongs to one app (ADR 0022) and sees only that app's profiles
|
|
4114
|
+
* and that app's events.
|
|
4115
|
+
*
|
|
4065
4116
|
* A row is either standalone (built in the dashboard, `journey_key` null) or
|
|
4066
4117
|
* owned by one journey whose trigger inlined its definition (ADR 0016). An
|
|
4067
4118
|
* owned row is named after its journey's key, is written only by the push,
|
|
@@ -4076,6 +4127,8 @@ const insertQuarantineEntrySchema = createInsertSchema(quarantineEntries);
|
|
|
4076
4127
|
const segments$1 = pgTable("segments", {
|
|
4077
4128
|
id: text("id").primaryKey(),
|
|
4078
4129
|
orgId: text("org_id").notNull(),
|
|
4130
|
+
/** The one app this segment evaluates. */
|
|
4131
|
+
appId: text("app_id").notNull(),
|
|
4079
4132
|
name: text("name").notNull(),
|
|
4080
4133
|
description: text("description"),
|
|
4081
4134
|
definition: jsonb("definition").$type().notNull(),
|
|
@@ -4090,7 +4143,12 @@ const segments$1 = pgTable("segments", {
|
|
|
4090
4143
|
columns: [table.orgId, table.journeyKey],
|
|
4091
4144
|
foreignColumns: [journeys$1.orgId, journeys$1.key],
|
|
4092
4145
|
name: "segments_journey_fk"
|
|
4093
|
-
}).onDelete("cascade")
|
|
4146
|
+
}).onDelete("cascade"),
|
|
4147
|
+
foreignKey({
|
|
4148
|
+
columns: [table.orgId, table.appId],
|
|
4149
|
+
foreignColumns: [apps$1.orgId, apps$1.id],
|
|
4150
|
+
name: "segments_app_fk"
|
|
4151
|
+
})
|
|
4094
4152
|
]);
|
|
4095
4153
|
/**
|
|
4096
4154
|
* Materialized membership: one row per (segment, profile) currently in the
|
|
@@ -4353,8 +4411,8 @@ const versions$1 = pgTable("versions", {
|
|
|
4353
4411
|
/** The file basename its author wrote; unique per org within a kind. */
|
|
4354
4412
|
key: text("key").notNull(),
|
|
4355
4413
|
kind: versionKindEnum("kind").notNull(),
|
|
4356
|
-
/** The
|
|
4357
|
-
|
|
4414
|
+
/** The app that pushed this key; ADR 0009's ownership, ADR 0022's app. */
|
|
4415
|
+
appId: text("app_id").notNull(),
|
|
4358
4416
|
/** The push that compiled this version's module. */
|
|
4359
4417
|
pushId: text("push_id").notNull().references(() => pushes$1.id),
|
|
4360
4418
|
moduleDigest: text("module_digest"),
|
|
@@ -4374,7 +4432,12 @@ const versions$1 = pgTable("versions", {
|
|
|
4374
4432
|
}, (table) => [
|
|
4375
4433
|
uniqueIndex("versions_org_id_kind_key_module_digest_unique").on(table.orgId, table.kind, table.key, table.moduleDigest),
|
|
4376
4434
|
index("versions_org_id_kind_key_pushed_at_idx").on(table.orgId, table.kind, table.key, table.pushedAt),
|
|
4377
|
-
index("versions_org_id_push_id_idx").on(table.orgId, table.pushId)
|
|
4435
|
+
index("versions_org_id_push_id_idx").on(table.orgId, table.pushId),
|
|
4436
|
+
foreignKey({
|
|
4437
|
+
columns: [table.orgId, table.appId],
|
|
4438
|
+
foreignColumns: [apps$1.orgId, apps$1.id],
|
|
4439
|
+
name: "versions_app_fk"
|
|
4440
|
+
})
|
|
4378
4441
|
]);
|
|
4379
4442
|
const selectVersionSchema = createSelectSchema(versions$1);
|
|
4380
4443
|
const insertVersionSchema = createInsertSchema(versions$1);
|
|
@@ -4557,8 +4620,6 @@ const ERROR_CODE_STATUS = {
|
|
|
4557
4620
|
journey_nondeterministic: 500,
|
|
4558
4621
|
push_invalid: 422,
|
|
4559
4622
|
version_not_ready: 409,
|
|
4560
|
-
project_exists: 409,
|
|
4561
|
-
project_missing: 404,
|
|
4562
4623
|
artifact_too_large: 413
|
|
4563
4624
|
};
|
|
4564
4625
|
/** First zod issue's message: enough signal for a 422 without a novel format. */
|
|
@@ -4612,6 +4673,21 @@ const identifierValueSchema = z.string().trim().min(1, "an identifier value is r
|
|
|
4612
4673
|
* and a call names only the kinds it has.
|
|
4613
4674
|
*/
|
|
4614
4675
|
const identifiersSchema = z.partialRecord(identifierKindSchema, identifierValueSchema).refine((value) => Object.keys(value).length > 0, { message: "identifiers must carry at least one entry" });
|
|
4676
|
+
/**
|
|
4677
|
+
* One identifier named as an object: how compliance addresses a person
|
|
4678
|
+
* when no `usr_` id is at hand (ADR 0022). A value two apps know is two
|
|
4679
|
+
* people, so the routes taking this act on every one of them.
|
|
4680
|
+
*
|
|
4681
|
+
* An object rather than the lookup route's `<kind>:<value>` string, because
|
|
4682
|
+
* the kind is then an enum at the boundary (a typo'd kind is a 422 for
|
|
4683
|
+
* free) and an address needs no escaping inside it. The value is
|
|
4684
|
+
* normalized by `normalizeIdentifierValue` where it is read, like every
|
|
4685
|
+
* other identifier.
|
|
4686
|
+
*/
|
|
4687
|
+
const identifierRefSchema = z.object({
|
|
4688
|
+
kind: identifierKindSchema,
|
|
4689
|
+
value: identifierValueSchema
|
|
4690
|
+
});
|
|
4615
4691
|
/** One identifier as the profile DTOs carry it. */
|
|
4616
4692
|
const identifierDtoSchema = z.object({
|
|
4617
4693
|
kind: identifierKindSchema,
|
|
@@ -4626,21 +4702,18 @@ const identifierDtoSchema = z.object({
|
|
|
4626
4702
|
* history, combined with `all` or `any`. Deliberately flat — nested predicate groups are YAGNI for the
|
|
4627
4703
|
* prototype, and a flat list keeps the pure evaluator a fold.
|
|
4628
4704
|
*
|
|
4629
|
-
*
|
|
4630
|
-
*
|
|
4631
|
-
* app
|
|
4632
|
-
*
|
|
4633
|
-
* A definition and the membership it produces are both the
|
|
4634
|
-
* organization's.
|
|
4705
|
+
* A definition names no app (ADR 0022): the segment row's own `app_id` is
|
|
4706
|
+
* the one app it evaluates, so the events and the profiles it reads are
|
|
4707
|
+
* that app's and there is nothing to filter by here.
|
|
4635
4708
|
*
|
|
4636
4709
|
* Apart from `./segments` because a journey's trigger carries a definition
|
|
4637
4710
|
* and the trigger schema is bundled into the wasm guest, where an edge to
|
|
4638
4711
|
* `@cowliss/db` (which `./segments` has, for the row schema) is a hard
|
|
4639
4712
|
* bundler failure. Nothing here imports anything but zod.
|
|
4640
4713
|
*
|
|
4641
|
-
* The object is strict: a definition holding the retired `sourceId`
|
|
4642
|
-
* (which meant the app) fails loudly instead of parsing
|
|
4643
|
-
* definition that evaluates over every app. There is deliberately no pipe
|
|
4714
|
+
* The object is strict: a definition holding the retired `sourceId` or
|
|
4715
|
+
* `appId` key (both of which meant the app) fails loudly instead of parsing
|
|
4716
|
+
* as an unfiltered definition that evaluates over every app. There is deliberately no pipe
|
|
4644
4717
|
* filter here — filtering by source is a later feature, and accepting one
|
|
4645
4718
|
* now would make a stale `sourceId` parse as a filter on a pipe that does
|
|
4646
4719
|
* not exist and silently match nothing.
|
|
@@ -4693,15 +4766,8 @@ const segmentPredicateSchema = z.discriminatedUnion("kind", [z.object({
|
|
|
4693
4766
|
/** Rolling window, relative to evaluation time; absent means all history. */
|
|
4694
4767
|
withinDays: z.number().int().min(1).optional()
|
|
4695
4768
|
})]).refine((predicate) => predicate.kind !== "trait" || VALUELESS_TRAIT_OPS.includes(predicate.op) || predicate.value !== void 0, { message: "value is required unless op is exists or notExists" });
|
|
4696
|
-
const appIdSchema = z.string().trim().min(1);
|
|
4697
4769
|
const segmentDefinitionSchema = z.strictObject({
|
|
4698
4770
|
match: z.enum(["all", "any"]).default("all"),
|
|
4699
|
-
/**
|
|
4700
|
-
* Optional app filter over the event side; null/absent spans apps.
|
|
4701
|
-
* A list matches any of the named apps, which is how one definition
|
|
4702
|
-
* names the development and the production id of the same app.
|
|
4703
|
-
*/
|
|
4704
|
-
appId: z.union([appIdSchema, z.array(appIdSchema).min(1)]).nullish(),
|
|
4705
4771
|
predicates: z.array(segmentPredicateSchema).min(1, "at least one predicate is required")
|
|
4706
4772
|
});
|
|
4707
4773
|
|
|
@@ -4814,9 +4880,12 @@ const purposesSchema = z.array(declaredPurposeSchema).max(20, "a project declare
|
|
|
4814
4880
|
const onePatternSchema = z.string().min(1).max(200, "a pattern must be at most 200 characters");
|
|
4815
4881
|
const patternSchema = z.union([onePatternSchema, z.array(onePatternSchema).min(1).max(20, "a matcher takes at most 20 patterns")]);
|
|
4816
4882
|
/**
|
|
4817
|
-
* What starts a journey: an event
|
|
4818
|
-
*
|
|
4819
|
-
*
|
|
4883
|
+
* What starts a journey: an event, or entry into the segment the trigger
|
|
4884
|
+
* itself describes. The registry DTO in `../journeys` reuses it.
|
|
4885
|
+
*
|
|
4886
|
+
* No trigger names an app (ADR 0022): a repository is one app, the journey
|
|
4887
|
+
* row carries its `app_id`, and the runtime compares that with the event's.
|
|
4888
|
+
* An `appId` here would be a second way to say it, and the way to leak.
|
|
4820
4889
|
*
|
|
4821
4890
|
* A segment trigger carries the predicate list, not a name: the push
|
|
4822
4891
|
* materializes one segment row per journey that inlines a definition, owned
|
|
@@ -4825,16 +4894,13 @@ const patternSchema = z.union([onePatternSchema, z.array(onePatternSchema).min(1
|
|
|
4825
4894
|
* definition is data — validated here, carried on the version, never
|
|
4826
4895
|
* compiled and never executed.
|
|
4827
4896
|
*
|
|
4828
|
-
* Both members are strict, so a journey holding the retired `source`
|
|
4829
|
-
* the retired `{ segment: "name" }` form, fails to build
|
|
4830
|
-
* triggering on every app or on nothing. There is
|
|
4831
|
-
*
|
|
4832
|
-
*
|
|
4897
|
+
* Both members are strict, so a journey holding the retired `source` or
|
|
4898
|
+
* `appId` key, or the retired `{ segment: "name" }` form, fails to build
|
|
4899
|
+
* instead of silently triggering on every app or on nothing. There is
|
|
4900
|
+
* deliberately no pipe filter and no app filter: the journey's own app is
|
|
4901
|
+
* the only narrowing there is.
|
|
4833
4902
|
*/
|
|
4834
|
-
const triggerSchema = z.union([z.strictObject({
|
|
4835
|
-
event: patternSchema,
|
|
4836
|
-
appId: patternSchema.optional()
|
|
4837
|
-
}), z.strictObject({ segment: segmentDefinitionSchema })]);
|
|
4903
|
+
const triggerSchema = z.union([z.strictObject({ event: patternSchema }), z.strictObject({ segment: segmentDefinitionSchema })]);
|
|
4838
4904
|
/**
|
|
4839
4905
|
* The address half of a from-header: a local part, an `@`, and a dotted
|
|
4840
4906
|
* domain. Deliberately narrower than RFC 5322 (no quoted local parts, no
|
|
@@ -5130,6 +5196,74 @@ const identifyDataSchema = identifyFieldsSchema.refine((data) => notFutureTimest
|
|
|
5130
5196
|
});
|
|
5131
5197
|
const identifyBodySchema = z.object({ data: identifyDataSchema });
|
|
5132
5198
|
|
|
5199
|
+
//#endregion
|
|
5200
|
+
//#region ../../packages/shared/src/journeys-v2/config.ts
|
|
5201
|
+
/**
|
|
5202
|
+
* An app id as `cow.json` carries it: the readable `app_<slug>`
|
|
5203
|
+
* `appIdFromName` derives, which is the one id a developer types by hand.
|
|
5204
|
+
*
|
|
5205
|
+
* It lives here rather than beside the app DTOs in ../apps because that
|
|
5206
|
+
* module imports the Drizzle schema, which neither the wasmtime guest nor
|
|
5207
|
+
* the Temporal workflow isolate can carry, and because `cow.json` is the
|
|
5208
|
+
* file a developer types it into. Everything that names an app on the wire
|
|
5209
|
+
* reuses it.
|
|
5210
|
+
*/
|
|
5211
|
+
const appIdSchema = z.string().trim().max(200).regex(new RegExp(`^${ID_PREFIXES.app}[a-z0-9]+(-[a-z0-9]+)*$`), "an app id looks like \"app_website\"");
|
|
5212
|
+
/**
|
|
5213
|
+
* A source id: the one inbound pipe an ingestion or send call names. It
|
|
5214
|
+
* lives beside `appIdSchema` for the same two reasons — the sources module
|
|
5215
|
+
* imports the Drizzle schema, and both are ids that travel on the wire — and
|
|
5216
|
+
* it has the same shape, so a bare `src_` is refused here rather than at the
|
|
5217
|
+
* database.
|
|
5218
|
+
*
|
|
5219
|
+
* The prefixes above and here are interpolated raw; `journeys-v2.test.ts`
|
|
5220
|
+
* asserts they are plain `<word>_` literals, so none of them can quietly
|
|
5221
|
+
* change what these patterns match.
|
|
5222
|
+
*/
|
|
5223
|
+
const sourceIdSchema = z.string().trim().max(200).regex(new RegExp(`^${ID_PREFIXES.source}[a-z0-9_]+$`), "a source id looks like \"src_01j8x…\"");
|
|
5224
|
+
/**
|
|
5225
|
+
* A Clerk organization id, as `cow.json` spells it. Exported beside
|
|
5226
|
+
* `appIdSchema` because the push body carries the org the file names so the
|
|
5227
|
+
* server can refuse a push aimed at another one (ADR 0022).
|
|
5228
|
+
*/
|
|
5229
|
+
const orgIdSchema = z.string().trim().max(200).startsWith(CLERK_ORG_SUBJECT_PREFIX, "orgId must be a Clerk org id");
|
|
5230
|
+
/**
|
|
5231
|
+
* `cow.json` (spec: Project layout): the org, and which of its apps this
|
|
5232
|
+
* repository pushes to. Auth never lives in the file. Strict, so a typo'd
|
|
5233
|
+
* key is a build error rather than a silently ignored setting — which is
|
|
5234
|
+
* also what turns the retired `project` key into a refusal (`cow build`
|
|
5235
|
+
* says what to rename it to).
|
|
5236
|
+
*/
|
|
5237
|
+
const cowConfigSchema = z.strictObject({
|
|
5238
|
+
$schema: z.url().optional(),
|
|
5239
|
+
orgId: orgIdSchema,
|
|
5240
|
+
/**
|
|
5241
|
+
* Which app of the org this repository pushes to (ADR 0022). The app is
|
|
5242
|
+
* the push unit: it owns the journey and template keys, and `cow
|
|
5243
|
+
* status`, `cow enable --all` and `cow pull` all resolve against it. One
|
|
5244
|
+
* repository is one app, so no journey file names one.
|
|
5245
|
+
*/
|
|
5246
|
+
appId: appIdSchema,
|
|
5247
|
+
/**
|
|
5248
|
+
* The consent purposes this repository declares. They are org-wide, so
|
|
5249
|
+
* two apps declaring one key must agree on its label and default or the
|
|
5250
|
+
* push is refused; a push adds and updates them and never deletes one,
|
|
5251
|
+
* because profiles hold answers against them.
|
|
5252
|
+
*/
|
|
5253
|
+
purposes: purposesSchema.optional(),
|
|
5254
|
+
/** Overrides the API the CLI talks to; the hosted product needs none. */
|
|
5255
|
+
apiUrl: z.url().optional(),
|
|
5256
|
+
/**
|
|
5257
|
+
* Overrides the dashboard `cow login` opens. Paired with `apiUrl`: a
|
|
5258
|
+
* login against one Cowliss's dashboard yields a token the other's API
|
|
5259
|
+
* rejects, so a config that names an API names its dashboard too.
|
|
5260
|
+
*/
|
|
5261
|
+
webUrl: z.url().optional()
|
|
5262
|
+
}).meta({
|
|
5263
|
+
title: "cow.json",
|
|
5264
|
+
description: "A cow project: the organization and the app it pushes to."
|
|
5265
|
+
});
|
|
5266
|
+
|
|
5133
5267
|
//#endregion
|
|
5134
5268
|
//#region ../../packages/shared/src/track.ts
|
|
5135
5269
|
/**
|
|
@@ -5189,7 +5323,7 @@ const trackBodySchema = z.object({ data: trackDataSchema });
|
|
|
5189
5323
|
*/
|
|
5190
5324
|
const listEventsQuerySchema = paginationQuerySchema.extend({
|
|
5191
5325
|
direction: sortDirectionSchema.default("desc"),
|
|
5192
|
-
appId:
|
|
5326
|
+
appId: appIdSchema.optional(),
|
|
5193
5327
|
sourceId: z.string().trim().min(1).max(200).optional(),
|
|
5194
5328
|
event: z.string().trim().min(1).max(200).optional(),
|
|
5195
5329
|
/**
|
|
@@ -5712,10 +5846,16 @@ const listDeliveriesQuerySchema = paginationQuerySchema.extend({
|
|
|
5712
5846
|
channel: deliveryChannelSchema.optional(),
|
|
5713
5847
|
/** Why the send happened: a journey, a test send, or a direct API send. */
|
|
5714
5848
|
kind: deliveryKindSchema.optional(),
|
|
5715
|
-
appId:
|
|
5849
|
+
appId: appIdSchema.optional(),
|
|
5716
5850
|
/** One person's deliveries, merged ids included, same as the event feed. */
|
|
5717
5851
|
profileId: z.string().trim().min(1).max(200).optional(),
|
|
5718
5852
|
/**
|
|
5853
|
+
* What one journey execution sent. Exact, not approximated from journey
|
|
5854
|
+
* and profile: a profile that enrolls twice has one execution per
|
|
5855
|
+
* enrollment, and the older one must not show the newer one's sends.
|
|
5856
|
+
*/
|
|
5857
|
+
executionId: z.string().trim().min(1).max(200).optional(),
|
|
5858
|
+
/**
|
|
5719
5859
|
* Only deliveries touched at or after this instant, by `updatedAt`: the
|
|
5720
5860
|
* cursor a tail (`cow dev`) polls with, so a send and its later feedback
|
|
5721
5861
|
* transition both arrive. Inclusive for the same reason as the execution
|
|
@@ -5794,10 +5934,10 @@ const domainDnsSetupSchema = z.object({
|
|
|
5794
5934
|
* braces, no dependency. A pattern field takes one pattern or a non-empty
|
|
5795
5935
|
* list of them, and a list is a disjunction.
|
|
5796
5936
|
*
|
|
5797
|
-
* It lives here rather than beside `
|
|
5798
|
-
* imports the Drizzle schema, which neither the wasmtime guest nor
|
|
5799
|
-
* Temporal workflow isolate can carry. This file imports one constant,
|
|
5800
|
-
* both can.
|
|
5937
|
+
* It lives here rather than beside `appIdFromName` in ./apps because that
|
|
5938
|
+
* module imports the Drizzle schema, which neither the wasmtime guest nor
|
|
5939
|
+
* the Temporal workflow isolate can carry. This file imports one constant,
|
|
5940
|
+
* so both can.
|
|
5801
5941
|
*
|
|
5802
5942
|
* The system rule: a name under SYSTEM_EVENT_PREFIX matches only a pattern
|
|
5803
5943
|
* whose *literal prefix* (the text before its first `*`) is itself under
|
|
@@ -6135,6 +6275,13 @@ const listExecutionsQuerySchema = paginationQuerySchema.extend({
|
|
|
6135
6275
|
status: executionStatusSchema.optional(),
|
|
6136
6276
|
profileId: z.string().trim().min(1).max(200).optional(),
|
|
6137
6277
|
/**
|
|
6278
|
+
* One app's runs. An execution carries no app of its own: it is a run of
|
|
6279
|
+
* a journey, and the journey is what belongs to an app (ADR 0022), so the
|
|
6280
|
+
* filter resolves through `journeys.app_id`. Omitted means every app, and
|
|
6281
|
+
* an id the org does not have matches nobody.
|
|
6282
|
+
*/
|
|
6283
|
+
appId: appIdSchema.optional(),
|
|
6284
|
+
/**
|
|
6138
6285
|
* Only executions touched at or after this instant, by `updatedAt`: what a
|
|
6139
6286
|
* tail (`cow dev`) asks for so it sees both new runs and status changes in
|
|
6140
6287
|
* one filter. Inclusive on purpose, because two rows can share a
|
|
@@ -6160,67 +6307,17 @@ const cancelExecutionsBodySchema = z.object({ data: z.object({
|
|
|
6160
6307
|
*/
|
|
6161
6308
|
const cancellingSchema = z.object({ cancelling: z.literal(true) });
|
|
6162
6309
|
|
|
6163
|
-
//#endregion
|
|
6164
|
-
//#region ../../packages/shared/src/journeys-v2/config.ts
|
|
6165
|
-
/**
|
|
6166
|
-
* A project's name: what `cow init` asked for, and what the `projects` row
|
|
6167
|
-
* stores. Lives here rather than beside the release DTOs because `cow.json`
|
|
6168
|
-
* is the file a developer types it into; `createProjectBodySchema` reuses it.
|
|
6169
|
-
*/
|
|
6170
|
-
const projectNameSchema = z.string().trim().min(1).max(200);
|
|
6171
|
-
/**
|
|
6172
|
-
* `cow.json` (spec: Project layout): the org, and which of its projects this
|
|
6173
|
-
* directory is. Auth never lives in the project. Strict, so a typo'd key
|
|
6174
|
-
* is a build error rather than a silently ignored setting.
|
|
6175
|
-
*/
|
|
6176
|
-
const cowConfigSchema = z.strictObject({
|
|
6177
|
-
$schema: z.url().optional(),
|
|
6178
|
-
orgId: z.string().startsWith(CLERK_ORG_SUBJECT_PREFIX, "orgId must be a Clerk org id"),
|
|
6179
|
-
/**
|
|
6180
|
-
* Which project of the org this directory pushes to. One org can hold
|
|
6181
|
-
* several, one per repo, each with its own release sequence and its own
|
|
6182
|
-
* slice of the deployed journeys, so every project says which it is.
|
|
6183
|
-
*/
|
|
6184
|
-
project: projectNameSchema,
|
|
6185
|
-
/**
|
|
6186
|
-
* The consent purposes this project declares. They are org-wide, so two
|
|
6187
|
-
* projects declaring one key must agree on its label and default or the
|
|
6188
|
-
* deploy is refused; a deploy adds and updates them and never deletes
|
|
6189
|
-
* one, because profiles hold answers against them.
|
|
6190
|
-
*/
|
|
6191
|
-
purposes: purposesSchema.optional(),
|
|
6192
|
-
/** Overrides the API the CLI talks to; the hosted product needs none. */
|
|
6193
|
-
apiUrl: z.url().optional(),
|
|
6194
|
-
/**
|
|
6195
|
-
* Overrides the dashboard `cow login` opens. Paired with `apiUrl`: a
|
|
6196
|
-
* login against one Cowliss's dashboard yields a token the other's API
|
|
6197
|
-
* rejects, so a config that names an API names its dashboard too.
|
|
6198
|
-
*/
|
|
6199
|
-
webUrl: z.url().optional()
|
|
6200
|
-
}).meta({
|
|
6201
|
-
title: "cow.json",
|
|
6202
|
-
description: "A cow project: the organization and the project it pushes to."
|
|
6203
|
-
});
|
|
6204
|
-
|
|
6205
6310
|
//#endregion
|
|
6206
6311
|
//#region ../../packages/shared/src/pushes.ts
|
|
6207
6312
|
/**
|
|
6208
|
-
* The push lifecycle's wire contracts (ADR 0011): the
|
|
6209
|
-
*
|
|
6210
|
-
* the
|
|
6313
|
+
* The push lifecycle's wire contracts (ADR 0011): the artifacts `cow push`
|
|
6314
|
+
* uploads, the push itself, and the versions its compile produces. The app
|
|
6315
|
+
* `cow.json` names is the push unit (ADR 0022).
|
|
6211
6316
|
*
|
|
6212
6317
|
* Row-derived through drizzle-zod where a row is what the wire carries, so
|
|
6213
6318
|
* the DTO cannot drift from the column set; the jsonb manifest gets its
|
|
6214
6319
|
* explicit shared schema, because the column type is opaque to drizzle-zod.
|
|
6215
6320
|
*/
|
|
6216
|
-
const projectSchema = selectProjectSchema.extend({
|
|
6217
|
-
createdAt: z.iso.datetime(),
|
|
6218
|
-
updatedAt: z.iso.datetime()
|
|
6219
|
-
});
|
|
6220
|
-
/** `cow init` names the project after the folder it created. */
|
|
6221
|
-
const createProjectBodySchema = z.object({ data: z.object({ name: projectNameSchema }) });
|
|
6222
|
-
/** Which project to read: the one the caller's `cow.json` names. */
|
|
6223
|
-
const getProjectQuerySchema = z.object({ name: projectNameSchema });
|
|
6224
6321
|
const artifactDigestParams = z.object({ digest: digestSchema });
|
|
6225
6322
|
/**
|
|
6226
6323
|
* Which kind of artifact the uploaded bytes are. Named by the client rather
|
|
@@ -6252,7 +6349,7 @@ const versionSummarySchema = selectVersionSchema.pick({
|
|
|
6252
6349
|
status: true,
|
|
6253
6350
|
moduleDigest: true,
|
|
6254
6351
|
pushId: true,
|
|
6255
|
-
|
|
6352
|
+
appId: true,
|
|
6256
6353
|
error: true
|
|
6257
6354
|
}).extend({ pushedAt: z.iso.datetime() });
|
|
6258
6355
|
/**
|
|
@@ -6281,12 +6378,21 @@ const createPushBodySchema = z.object({ data: z.object({
|
|
|
6281
6378
|
* the developer fixes by updating `@cowliss/cli`, and nothing is stored.
|
|
6282
6379
|
*/
|
|
6283
6380
|
manifest: manifestSchema.safeExtend({ protocol: z.literal(3) }),
|
|
6284
|
-
/** The `cow.json`
|
|
6285
|
-
|
|
6381
|
+
/** The `cow.json` app this push belongs to (ADR 0022). */
|
|
6382
|
+
appId: appIdSchema,
|
|
6383
|
+
/**
|
|
6384
|
+
* The `cow.json` organization. The credential already decides which org
|
|
6385
|
+
* is written to, so this is not how the server finds the org: it is the
|
|
6386
|
+
* developer saying which one they meant, and a disagreement is refused.
|
|
6387
|
+
* Without it, a push aimed at production but run with local credentials
|
|
6388
|
+
* lands silently whenever both orgs happen to hold an app of the same
|
|
6389
|
+
* slug, which is exactly the shape `apps/platform-workspace` has.
|
|
6390
|
+
*/
|
|
6391
|
+
orgId: orgIdSchema
|
|
6286
6392
|
}) });
|
|
6287
6393
|
const listPushesQuerySchema = paginationQuerySchema.extend({
|
|
6288
|
-
/** One
|
|
6289
|
-
|
|
6394
|
+
/** One app's own pushes; absent lists the org's. */
|
|
6395
|
+
appId: appIdSchema.optional() });
|
|
6290
6396
|
/**
|
|
6291
6397
|
* One key's version history, newest first. Both the key and the kind are
|
|
6292
6398
|
* required: journeys and templates share a key space, so `welcome` alone
|
|
@@ -6297,13 +6403,13 @@ const listVersionsQuerySchema = paginationQuerySchema.extend({
|
|
|
6297
6403
|
kind: z.enum(["journey", "template"])
|
|
6298
6404
|
});
|
|
6299
6405
|
/**
|
|
6300
|
-
* One key of
|
|
6406
|
+
* One key of an app as `cow status` and the MCP `status` tool report it:
|
|
6301
6407
|
* the code the server holds for it, what state it is in, and what is still
|
|
6302
6408
|
* running. Journeys and templates share the shape, because a developer asks
|
|
6303
6409
|
* the same question of both; the two fields only a journey has are null on a
|
|
6304
6410
|
* template.
|
|
6305
6411
|
*/
|
|
6306
|
-
const
|
|
6412
|
+
const appStatusKeySchema = z.object({
|
|
6307
6413
|
kind: z.enum(["journey", "template"]),
|
|
6308
6414
|
key: z.string(),
|
|
6309
6415
|
/**
|
|
@@ -6322,19 +6428,19 @@ const projectStatusKeySchema = z.object({
|
|
|
6322
6428
|
liveExecutions: z.number().int()
|
|
6323
6429
|
});
|
|
6324
6430
|
/**
|
|
6325
|
-
* Everything the server knows about one
|
|
6326
|
-
*
|
|
6431
|
+
* Everything the server knows about one app's keys, in one read: the whole
|
|
6432
|
+
* app rather than a page of it, because a manifest holds at most
|
|
6327
6433
|
* `PUSH_LIMITS.journeys + PUSH_LIMITS.templates` keys and the answer to "what
|
|
6328
|
-
* is my
|
|
6434
|
+
* is my app doing" is useless split across cursors.
|
|
6329
6435
|
*
|
|
6330
6436
|
* `warnings` is what a deploy used to answer with (ADR 0011 retired the
|
|
6331
6437
|
* deploy): a name a journey references that the organization does not
|
|
6332
6438
|
* define yet. They never block, because a segment or a webhook may be created
|
|
6333
6439
|
* right after a push.
|
|
6334
6440
|
*/
|
|
6335
|
-
const
|
|
6336
|
-
|
|
6337
|
-
keys: z.array(
|
|
6441
|
+
const appStatusSchema = z.object({
|
|
6442
|
+
appId: appIdSchema,
|
|
6443
|
+
keys: z.array(appStatusKeySchema),
|
|
6338
6444
|
warnings: z.array(z.string())
|
|
6339
6445
|
});
|
|
6340
6446
|
|
|
@@ -6355,15 +6461,20 @@ const segmentMemberSchema = z.object({
|
|
|
6355
6461
|
});
|
|
6356
6462
|
/**
|
|
6357
6463
|
* The builder's design-time sanity check: run one not-yet-persisted
|
|
6358
|
-
* definition over
|
|
6359
|
-
*
|
|
6360
|
-
*
|
|
6464
|
+
* definition over one app's existing profiles. The app is the same one the
|
|
6465
|
+
* segment would be created in (ADR 0022), so the count the builder shows is
|
|
6466
|
+
* the count saving it would produce. Members is a small sample of matching
|
|
6467
|
+
* profile ids, not the full member list: there is no segment row and no
|
|
6468
|
+
* membership entry, so there is no segmentId or enteredAt to report.
|
|
6361
6469
|
*
|
|
6362
|
-
* The scan is bounded, so `memberCount` is the exact
|
|
6470
|
+
* The scan is bounded, so `memberCount` is the exact count for that app only
|
|
6363
6471
|
* when `truncated` is false. When it is true the scan stopped at the cap,
|
|
6364
6472
|
* and `memberCount` is the count over the first `scanned` profiles only.
|
|
6365
6473
|
*/
|
|
6366
|
-
const previewSegmentBodySchema = z.object({ data:
|
|
6474
|
+
const previewSegmentBodySchema = z.object({ data: z.object({
|
|
6475
|
+
appId: appIdSchema,
|
|
6476
|
+
definition: segmentDefinitionSchema
|
|
6477
|
+
}) });
|
|
6367
6478
|
const segmentPreviewSchema = z.object({
|
|
6368
6479
|
memberCount: z.number().int().min(0),
|
|
6369
6480
|
members: z.array(z.string()),
|
|
@@ -6374,8 +6485,15 @@ const segmentPreviewSchema = z.object({
|
|
|
6374
6485
|
});
|
|
6375
6486
|
const nameSchema = z.string().trim().min(1, "name is required").max(100, "name must be at most 100 characters");
|
|
6376
6487
|
const descriptionSchema = z.string().trim().max(500, `description must be at most ${500} characters`);
|
|
6488
|
+
/**
|
|
6489
|
+
* A segment belongs to exactly one app (ADR 0022), named here and never
|
|
6490
|
+
* again: it evaluates that app's profiles and that app's events, and an
|
|
6491
|
+
* update cannot move it, because the members it already has belong to the
|
|
6492
|
+
* app it was built for.
|
|
6493
|
+
*/
|
|
6377
6494
|
const createSegmentBodySchema = z.object({ data: z.object({
|
|
6378
6495
|
name: nameSchema,
|
|
6496
|
+
appId: appIdSchema,
|
|
6379
6497
|
description: descriptionSchema.nullish(),
|
|
6380
6498
|
definition: segmentDefinitionSchema
|
|
6381
6499
|
}) });
|
|
@@ -6384,8 +6502,15 @@ const updateSegmentBodySchema = z.object({ data: z.object({
|
|
|
6384
6502
|
description: descriptionSchema.nullish(),
|
|
6385
6503
|
definition: segmentDefinitionSchema.optional()
|
|
6386
6504
|
}).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" }) });
|
|
6387
|
-
/**
|
|
6388
|
-
|
|
6505
|
+
/**
|
|
6506
|
+
* `q` is a substring search over the segment's name; `appId` narrows to one
|
|
6507
|
+
* app's segments, and omitted means every app. An id the org does not have
|
|
6508
|
+
* matches nobody rather than 404ing, like every other list filter.
|
|
6509
|
+
*/
|
|
6510
|
+
const listSegmentsQuerySchema = paginationQuerySchema.extend({
|
|
6511
|
+
q: searchQuerySchema,
|
|
6512
|
+
appId: appIdSchema.optional()
|
|
6513
|
+
});
|
|
6389
6514
|
const listSegmentMembersQuerySchema = paginationQuerySchema.extend({});
|
|
6390
6515
|
|
|
6391
6516
|
//#endregion
|
|
@@ -6445,10 +6570,15 @@ const journeyDetailSchema = journeySchema.extend({ segment: segmentDetailSchema.
|
|
|
6445
6570
|
* the two things an author names a journey by; `tag` is "has this tag",
|
|
6446
6571
|
* exact and case-sensitive, so a badge in the table is the way into it.
|
|
6447
6572
|
* Both are server-side, like every other list filter.
|
|
6573
|
+
*
|
|
6574
|
+
* `appId` narrows to one app's journeys (ADR 0022); omitted means every
|
|
6575
|
+
* app. An id the org does not have matches nobody, the same empty answer
|
|
6576
|
+
* `journeys.setStatus` gives it.
|
|
6448
6577
|
*/
|
|
6449
6578
|
const listJourneysQuerySchema = paginationQuerySchema.extend({
|
|
6450
6579
|
/** On, off or paused; the journey's one state. */
|
|
6451
6580
|
status: journeyStatusSchema.optional(),
|
|
6581
|
+
appId: appIdSchema.optional(),
|
|
6452
6582
|
q: z.string().trim().max(200, "q must be at most 200 characters").optional().transform((value) => value ? value : void 0),
|
|
6453
6583
|
tag: z.string().trim().max(50, "tag must be at most 50 characters").optional()
|
|
6454
6584
|
});
|
|
@@ -6477,13 +6607,13 @@ const journeyStatsSchema = z.object({
|
|
|
6477
6607
|
});
|
|
6478
6608
|
/**
|
|
6479
6609
|
* Which journeys the status acts on, and which state to put them in: the
|
|
6480
|
-
* keys the caller named, or every journey of one
|
|
6610
|
+
* keys the caller named, or every journey of one app. Exactly one of the
|
|
6481
6611
|
* two selectors, because a call carrying both would have to decide which it
|
|
6482
6612
|
* meant.
|
|
6483
6613
|
*
|
|
6484
6614
|
* A list rather than one key per call so every surface can do what `cow
|
|
6485
|
-
* enable` does: name several keys, or `--all` (which is `
|
|
6486
|
-
* server-side, so an agent turning
|
|
6615
|
+
* enable` does: name several keys, or `--all` (which is `appId`, resolved
|
|
6616
|
+
* server-side, so an agent turning an app on is one call too).
|
|
6487
6617
|
*
|
|
6488
6618
|
* One body for three states rather than a route per verb (ADR 0018): the
|
|
6489
6619
|
* transitions differ in what they do to what is in flight, not in who may
|
|
@@ -6491,10 +6621,10 @@ const journeyStatsSchema = z.object({
|
|
|
6491
6621
|
*/
|
|
6492
6622
|
const setJourneysStatusBodySchema = z.object({ data: z.object({
|
|
6493
6623
|
keys: z.array(journeyKeySchema).min(1).max(PUSH_LIMITS.journeys).optional(),
|
|
6494
|
-
/** Every journey of this
|
|
6495
|
-
|
|
6624
|
+
/** Every journey of this app, by the id `cow.json` carries. */
|
|
6625
|
+
appId: appIdSchema.optional(),
|
|
6496
6626
|
status: journeyStatusSchema
|
|
6497
|
-
}).refine((data) => data.keys === void 0 !== (data.
|
|
6627
|
+
}).refine((data) => data.keys === void 0 !== (data.appId === void 0), { message: "name keys or an app, not both" }) });
|
|
6498
6628
|
/**
|
|
6499
6629
|
* Dry run: one real profile, the journey's latest ready version, and every
|
|
6500
6630
|
* send gated into a `would_*` row instead of a message. The body is the v1
|
|
@@ -6982,7 +7112,12 @@ const templateSchema$1 = z.strictObject({
|
|
|
6982
7112
|
props: z.record(z.string(), z.unknown()).optional()
|
|
6983
7113
|
});
|
|
6984
7114
|
const sendEmailDataSchema = z.strictObject({
|
|
6985
|
-
|
|
7115
|
+
/**
|
|
7116
|
+
* The pipe the message belongs to. A source id and only a source id: the
|
|
7117
|
+
* Resend facade takes an app id in its base URL because the Resend SDK
|
|
7118
|
+
* has nowhere else to put one, and nothing else on the wire does.
|
|
7119
|
+
*/
|
|
7120
|
+
sourceId: sourceIdSchema,
|
|
6986
7121
|
to: recipientsSchema,
|
|
6987
7122
|
cc: recipientsSchema.optional(),
|
|
6988
7123
|
bcc: recipientsSchema.optional(),
|
|
@@ -7184,8 +7319,8 @@ const listSuppressionsQuerySchema = paginationQuerySchema.extend({ q: searchQuer
|
|
|
7184
7319
|
*/
|
|
7185
7320
|
const templateSchema = z.object({
|
|
7186
7321
|
key: journeyKeySchema,
|
|
7187
|
-
/** The
|
|
7188
|
-
|
|
7322
|
+
/** The app whose push owns this key (ADR 0009, ADR 0022). */
|
|
7323
|
+
appId: z.string(),
|
|
7189
7324
|
/** The author's labels; the dashboard's only grouping. */
|
|
7190
7325
|
tags: tagsSchema,
|
|
7191
7326
|
sendClass: z.enum(SEND_CLASSES),
|
|
@@ -7271,10 +7406,17 @@ const listUserRunsQuerySchema = paginationQuerySchema;
|
|
|
7271
7406
|
* as text (the jsonb value rendered with ->>), so "who is on the pro plan" is
|
|
7272
7407
|
* one request. `traitValue` without `trait` is meaningless and is a 422: the
|
|
7273
7408
|
* value names no key to look in.
|
|
7409
|
+
*
|
|
7410
|
+
* `appId` narrows to one app's profiles (ADR 0022); omitted means every app.
|
|
7411
|
+
* An id the org does not have simply matches nobody, like `segmentId`. `q`
|
|
7412
|
+
* still searches identifier values across the org, so an address two apps
|
|
7413
|
+
* know answers with one profile per app.
|
|
7274
7414
|
*/
|
|
7275
7415
|
const listUsersQuerySchema = paginationQuerySchema.extend({
|
|
7276
7416
|
direction: sortDirectionSchema.default("asc"),
|
|
7277
7417
|
q: searchQuerySchema,
|
|
7418
|
+
/** One app's profiles. An unknown id matches nobody, it is not a 404. */
|
|
7419
|
+
appId: appIdSchema.optional(),
|
|
7278
7420
|
trait: z.string().trim().min(1).max(200).optional(),
|
|
7279
7421
|
traitValue: z.string().trim().min(1).max(500).optional(),
|
|
7280
7422
|
/** Members of one segment. An unknown id matches nobody, it is not a 404. */
|
|
@@ -7300,13 +7442,32 @@ const findUserQuerySchema = z.object({ identifier: z.string().trim().min(3, "ide
|
|
|
7300
7442
|
const userDetailSchema = profileDtoSchema.extend({ mergedIds: z.array(z.string()) });
|
|
7301
7443
|
const updateConsentBodySchema = z.object({ data: consentPatchSchema });
|
|
7302
7444
|
/**
|
|
7303
|
-
*
|
|
7304
|
-
*
|
|
7445
|
+
* What erasure did, so the dashboard action can say "n journey instances
|
|
7446
|
+
* terminated". `profileIds` is the people that were erased: one id for
|
|
7447
|
+
* `DELETE /v1/users/:profileId`, one per app for an erasure by identifier
|
|
7448
|
+
* (ADR 0022). They are the survivor ids, the ones anyone could have
|
|
7449
|
+
* addressed; the merged-away ids that went with them are not listed,
|
|
7450
|
+
* because nothing ever showed them as a person.
|
|
7305
7451
|
*/
|
|
7306
7452
|
const eraseUserResponseSchema = z.object({
|
|
7307
|
-
|
|
7453
|
+
profileIds: z.array(z.string()),
|
|
7308
7454
|
terminatedInstances: z.number().int().min(0)
|
|
7309
7455
|
});
|
|
7456
|
+
/**
|
|
7457
|
+
* Erase everyone one identifier names (POST /v1/users/erase), across the
|
|
7458
|
+
* organization's apps. Same erasure as by id, addressed by
|
|
7459
|
+
* `{ kind, value }` because that is what a person asking to be forgotten
|
|
7460
|
+
* gives you; a value nobody in the org carries is a 404.
|
|
7461
|
+
*/
|
|
7462
|
+
const eraseUserByIdentifierBodySchema = z.object({ data: identifierRefSchema });
|
|
7463
|
+
/**
|
|
7464
|
+
* The same addressing for the export (POST /v1/users/export). A body rather
|
|
7465
|
+
* than a query string for the same reason erasure uses one: a query string
|
|
7466
|
+
* is part of the URL, and an access log, a proxy log and a browser history
|
|
7467
|
+
* all keep it, so an address must not travel there. The method reads
|
|
7468
|
+
* nothing but says where the identifier goes.
|
|
7469
|
+
*/
|
|
7470
|
+
const exportUserByIdentifierBodySchema = z.object({ data: identifierRefSchema });
|
|
7310
7471
|
/** One address-ledger row on the export; dates are ISO 8601 strings. */
|
|
7311
7472
|
const emailAddressSchema = selectEmailAddressSchema.extend({
|
|
7312
7473
|
verifiedAt: z.iso.datetime().nullable(),
|
|
@@ -7319,14 +7480,20 @@ const journeyRunDtoSchema = selectJourneyRunSchema.extend({
|
|
|
7319
7480
|
createdAt: z.iso.datetime()
|
|
7320
7481
|
});
|
|
7321
7482
|
/**
|
|
7322
|
-
*
|
|
7323
|
-
*
|
|
7324
|
-
*
|
|
7325
|
-
*
|
|
7483
|
+
* The identity's whole data record in one JSON document (GDPR
|
|
7484
|
+
* portability). Each section reuses the wire DTO of the store it came
|
|
7485
|
+
* from, so the export cannot drift from what the dashboard already shows.
|
|
7486
|
+
*
|
|
7487
|
+
* `users` is who the document is about: one profile for
|
|
7488
|
+
* `GET /v1/users/:profileId/export`, one per app when the export is
|
|
7489
|
+
* addressed by identifier (ADR 0022). The other sections are the union over
|
|
7490
|
+
* all of them and over everything merged into them, because that is the
|
|
7491
|
+
* data the request asked for, and because a person's export is one file
|
|
7492
|
+
* whether they used one product or three.
|
|
7326
7493
|
*/
|
|
7327
7494
|
const userExportSchema = z.object({
|
|
7328
7495
|
exportedAt: z.iso.datetime(),
|
|
7329
|
-
|
|
7496
|
+
users: z.array(userDetailSchema),
|
|
7330
7497
|
events: z.array(eventDtoSchema),
|
|
7331
7498
|
journeys: z.array(journeyRunDtoSchema),
|
|
7332
7499
|
deliveries: z.array(deliverySchema),
|
|
@@ -7969,11 +8136,15 @@ const CONFIG_FILES = [
|
|
|
7969
8136
|
* cannot be inlined). There is no `src/` fallback: a missing
|
|
7970
8137
|
* build surfaces here as an unresolved import rather than as a second code
|
|
7971
8138
|
* path whose only job is to hide it.
|
|
8139
|
+
*
|
|
8140
|
+
* `guest` and `guest-emails` are the two drivers and a bundle links exactly
|
|
8141
|
+
* one, picked by source kind (`DRIVER`).
|
|
7972
8142
|
*/
|
|
7973
8143
|
const GUEST_ALIAS = {
|
|
7974
8144
|
"@cowliss/cli/journeys": guestFile("journeys"),
|
|
7975
8145
|
"@cowliss/cli/emails": guestFile("emails"),
|
|
7976
8146
|
"@cowliss/cli/guest": guestFile("guest"),
|
|
8147
|
+
"@cowliss/cli/guest-emails": guestFile("guest-emails"),
|
|
7977
8148
|
"@cowliss/cli/guest-wasi": guestFile("guest-wasi"),
|
|
7978
8149
|
"@cowliss/cli/guest-prelude": guestFile("guest-prelude")
|
|
7979
8150
|
};
|
|
@@ -8158,23 +8329,33 @@ async function bundle(projectDir, outFile, entry) {
|
|
|
8158
8329
|
};
|
|
8159
8330
|
}
|
|
8160
8331
|
/**
|
|
8332
|
+
* The driver a bundle links, one per kind. Only the template one imports
|
|
8333
|
+
* the renderer, so naming it from a journey entry is what used to put react
|
|
8334
|
+
* and react-dom in all 95 journey bundles (`guest/driver.ts`).
|
|
8335
|
+
*/
|
|
8336
|
+
const DRIVER = {
|
|
8337
|
+
journeys: "@cowliss/cli/guest",
|
|
8338
|
+
emails: "@cowliss/cli/guest-emails"
|
|
8339
|
+
};
|
|
8340
|
+
/**
|
|
8161
8341
|
* The artifact the sandbox compiles: stdin in, stdout out, no exports. The
|
|
8162
8342
|
* prelude is imported first on purpose: ESM evaluates in import order, and
|
|
8163
8343
|
* its `MessageChannel` and `console` walls have to stand before react-dom
|
|
8164
8344
|
* or tenant code initializes.
|
|
8165
8345
|
*/
|
|
8166
|
-
function wasiEntry(file) {
|
|
8346
|
+
function wasiEntry(file, kind) {
|
|
8167
8347
|
return `import "@cowliss/cli/guest-prelude";
|
|
8168
8348
|
import * as module from ${JSON.stringify(file)};
|
|
8169
8349
|
import { main } from "@cowliss/cli/guest-wasi";
|
|
8170
|
-
|
|
8350
|
+
import { runGuest } from ${JSON.stringify(DRIVER[kind])};
|
|
8351
|
+
main(module, runGuest);
|
|
8171
8352
|
`;
|
|
8172
8353
|
}
|
|
8173
8354
|
/** The local twin: the same graph, importable by Node for the manifest and `cow test`. */
|
|
8174
|
-
function nodeEntry(file) {
|
|
8355
|
+
function nodeEntry(file, kind) {
|
|
8175
8356
|
return `import * as module from ${JSON.stringify(file)};
|
|
8176
8357
|
export { module };
|
|
8177
|
-
export { runGuest } from
|
|
8358
|
+
export { runGuest } from ${JSON.stringify(DRIVER[kind])};
|
|
8178
8359
|
`;
|
|
8179
8360
|
}
|
|
8180
8361
|
let messagePortsUnreffed = false;
|
|
@@ -8278,7 +8459,9 @@ async function assertCowConfig(projectDir) {
|
|
|
8278
8459
|
} catch {
|
|
8279
8460
|
throw new Error(name === "cow.json" ? `No ${name} in "${projectDir}". Run \`cow init\` to create a project.` : `No ${name} in "${projectDir}". That file was selected with --config or COW_CONFIG.`);
|
|
8280
8461
|
}
|
|
8281
|
-
const
|
|
8462
|
+
const raw = JSON.parse(text);
|
|
8463
|
+
if (typeof raw === "object" && raw !== null && "project" in raw) throw new Error(`${name} names a "project", which Cowliss no longer has: a repository pushes to one app. Rename that field to "appId" and give it the app's id, which looks like "app_website" and is on the app's page in the dashboard.`);
|
|
8464
|
+
const parsed = cowConfigSchema.safeParse(raw);
|
|
8282
8465
|
if (!parsed.success) throw new Error(`${name} is invalid: ${parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ")}`);
|
|
8283
8466
|
return parsed.data;
|
|
8284
8467
|
}
|
|
@@ -8291,14 +8474,14 @@ async function readCowConfig(projectDir) {
|
|
|
8291
8474
|
}
|
|
8292
8475
|
}
|
|
8293
8476
|
/**
|
|
8294
|
-
* Which
|
|
8477
|
+
* Which app of the org this directory pushes to: the `appId` in its
|
|
8295
8478
|
* `cow.json`, or undefined when there is no `cow.json` here at all. The
|
|
8296
|
-
* undefined case is `cow pull` into an empty directory, which has no
|
|
8297
|
-
*
|
|
8479
|
+
* undefined case is `cow pull` into an empty directory, which has no app to
|
|
8480
|
+
* scope by yet and reads the org's pushes.
|
|
8298
8481
|
*/
|
|
8299
|
-
async function
|
|
8482
|
+
async function configuredApp(projectDir) {
|
|
8300
8483
|
try {
|
|
8301
|
-
return (await assertCowConfig(projectDir)).
|
|
8484
|
+
return (await assertCowConfig(projectDir)).appId;
|
|
8302
8485
|
} catch {
|
|
8303
8486
|
return;
|
|
8304
8487
|
}
|
|
@@ -8326,7 +8509,7 @@ async function buildProject(projectDir) {
|
|
|
8326
8509
|
const bundles = await Promise.all(files.map(async (source) => {
|
|
8327
8510
|
const nodeFile = join(outDir, "node", source.kind, `${source.key}.mjs`);
|
|
8328
8511
|
const wasiFile = join(outDir, "bundles", source.kind, `${source.key}.js`);
|
|
8329
|
-
const [wasi, node] = await Promise.all([bundle(projectDir, wasiFile, wasiEntry(source.file)), bundle(projectDir, nodeFile, nodeEntry(source.file))]);
|
|
8512
|
+
const [wasi, node] = await Promise.all([bundle(projectDir, wasiFile, wasiEntry(source.file, source.kind)), bundle(projectDir, nodeFile, nodeEntry(source.file, source.kind))]);
|
|
8330
8513
|
await writeFile(nodeFile, node.code);
|
|
8331
8514
|
await writeFile(`${nodeFile}.map`, node.map);
|
|
8332
8515
|
await writeFile(wasiFile, wasi.code);
|
|
@@ -8569,7 +8752,7 @@ async function copyExample(name, dir, force) {
|
|
|
8569
8752
|
return files;
|
|
8570
8753
|
}
|
|
8571
8754
|
function registerAdd(program, io) {
|
|
8572
|
-
program.command("add").description("add gallery example code to a cow
|
|
8755
|
+
program.command("add").description("add gallery example code to a cow repo").command("example <name>").description("copy an example's journeys, emails, and scenarios into this repo (local: no API call)").option("--force", "overwrite files that already exist").action(async (name, opts) => {
|
|
8573
8756
|
const dir = process.cwd();
|
|
8574
8757
|
await assertCowConfig(dir);
|
|
8575
8758
|
const files = await copyExample(name, dir, opts.force === true);
|
|
@@ -8598,10 +8781,13 @@ async function login(env, options) {
|
|
|
8598
8781
|
const claims = decodeSessionToken(token);
|
|
8599
8782
|
if (claims === null) throw new Error("Token is not a decodable JWT");
|
|
8600
8783
|
if (claims.userId === null) throw new Error("Token has no subject claim. Is it a Clerk session token?");
|
|
8601
|
-
const
|
|
8602
|
-
if (options.apiFlag !== void 0) credentials.apiUrl = options.apiFlag;
|
|
8784
|
+
const apiUrl = options.apiFlag ?? resolveApiUrl(env, null, void 0, (await readCowConfig(process.cwd()))?.apiUrl);
|
|
8603
8785
|
return {
|
|
8604
|
-
credentialsPath: await writeCredentials(env,
|
|
8786
|
+
credentialsPath: await writeCredentials(env, {
|
|
8787
|
+
token,
|
|
8788
|
+
apiUrl
|
|
8789
|
+
}),
|
|
8790
|
+
apiUrl,
|
|
8605
8791
|
userId: claims.userId,
|
|
8606
8792
|
orgId: claims.orgId,
|
|
8607
8793
|
role: claims.role,
|
|
@@ -8688,7 +8874,7 @@ function openBrowser(url) {
|
|
|
8688
8874
|
//#region src/commands/auth.ts
|
|
8689
8875
|
/** login/logout/whoami: session-token management, no contract route. */
|
|
8690
8876
|
function registerAuth(program, env, io) {
|
|
8691
|
-
program.command("login").description("authenticate via the dashboard browser flow (or --token) and cache the session token").option("--token <jwt>", "paste a session token instead of the browser flow").option("--web <url>", `dashboard URL; overrides COW_WEB_URL and webUrl in
|
|
8877
|
+
program.command("login").description("authenticate via the dashboard browser flow (or --token) and cache the session token").option("--token <jwt>", "paste a session token instead of the browser flow").option("--web <url>", `dashboard URL; overrides COW_WEB_URL and webUrl in cow.json (default ${DEFAULT_WEB_URL})`).option("--no-open", "do not open the browser; print the URL and wait (headless/agent use)").action(async (opts) => {
|
|
8692
8878
|
const outcome = await login(env, {
|
|
8693
8879
|
token: opts.token,
|
|
8694
8880
|
webUrl: opts.web,
|
|
@@ -8718,8 +8904,14 @@ function registerAuth(program, env, io) {
|
|
|
8718
8904
|
}
|
|
8719
8905
|
const claims = decodeSessionToken(credentials?.token ?? "");
|
|
8720
8906
|
if (!claims) throw new Error("Cached token is not decodable. Run `cow login` again.");
|
|
8907
|
+
const projectApiUrl = (await readCowConfig(process.cwd()))?.apiUrl;
|
|
8908
|
+
const apiUrl = resolveApiUrl(env, credentials, opts.api, projectApiUrl);
|
|
8909
|
+
const mintedFor = credentials?.apiUrl;
|
|
8721
8910
|
emit({ data: {
|
|
8722
8911
|
credential: kind,
|
|
8912
|
+
apiUrl,
|
|
8913
|
+
mintedFor: mintedFor ?? null,
|
|
8914
|
+
...mintedFor !== void 0 && mintedFor !== apiUrl ? { warning: `This token was minted for ${mintedFor}, but commands here talk to ${apiUrl}. Run \`cow login\` again from this directory.` } : {},
|
|
8723
8915
|
...claims
|
|
8724
8916
|
} }, io, opts.json);
|
|
8725
8917
|
});
|
|
@@ -8732,7 +8924,7 @@ function registerAuth(program, env, io) {
|
|
|
8732
8924
|
* the current directory. Local only, no API call and no network.
|
|
8733
8925
|
*/
|
|
8734
8926
|
function registerBuild(program, io) {
|
|
8735
|
-
program.command("build").description("typecheck and bundle the
|
|
8927
|
+
program.command("build").description("typecheck and bundle the repo in the current directory, writing .cow/build (local: no API call)").action(async (opts) => {
|
|
8736
8928
|
emit({ data: await buildProject(process.cwd()) }, io, opts.json);
|
|
8737
8929
|
});
|
|
8738
8930
|
}
|
|
@@ -9165,7 +9357,8 @@ function htmlResponse(description) {
|
|
|
9165
9357
|
const params$11 = z.object({ id: z.string() });
|
|
9166
9358
|
/**
|
|
9167
9359
|
* An app is the attribution unit: what a developer names, what ingestion
|
|
9168
|
-
* stamps, and what
|
|
9360
|
+
* stamps, and what a profile, a segment and a journey belong to (ADR 0022).
|
|
9361
|
+
* It is also the push unit, which is what `status` reports on. Its inbound
|
|
9169
9362
|
* pipes live under /v1/apps/{appId}/sources, served by the sources module.
|
|
9170
9363
|
*/
|
|
9171
9364
|
const apps = defineModule(defineRoute({
|
|
@@ -9223,6 +9416,23 @@ const apps = defineModule(defineRoute({
|
|
|
9223
9416
|
...sessionErrors,
|
|
9224
9417
|
...errors("not_found", "validation_failed", "malformed_request")
|
|
9225
9418
|
}
|
|
9419
|
+
}), defineRoute({
|
|
9420
|
+
method: "get",
|
|
9421
|
+
path: "/v1/apps/{id}/status",
|
|
9422
|
+
operationId: "apps.status",
|
|
9423
|
+
tags: ["apps"],
|
|
9424
|
+
summary: "What an app's journeys and templates are doing",
|
|
9425
|
+
security: PIPELINE_AUTH,
|
|
9426
|
+
surfaces: {
|
|
9427
|
+
cli: false,
|
|
9428
|
+
mcpName: "status"
|
|
9429
|
+
},
|
|
9430
|
+
request: { params: params$11 },
|
|
9431
|
+
responses: {
|
|
9432
|
+
200: envelope(appStatusSchema),
|
|
9433
|
+
...sessionErrors,
|
|
9434
|
+
...errors("not_found", "validation_failed")
|
|
9435
|
+
}
|
|
9226
9436
|
}));
|
|
9227
9437
|
|
|
9228
9438
|
//#endregion
|
|
@@ -9514,7 +9724,7 @@ const catalog = defineModule(defineRoute({
|
|
|
9514
9724
|
//#region ../../packages/shared/src/contract/consent.ts
|
|
9515
9725
|
/**
|
|
9516
9726
|
* The consent purposes an organization's profiles answer: the two fixed ones
|
|
9517
|
-
* every org has, plus whatever its
|
|
9727
|
+
* every org has, plus whatever its apps declare in their `cow.json`.
|
|
9518
9728
|
*
|
|
9519
9729
|
* Read-only, and deliberately so. Purposes are code, like journeys and
|
|
9520
9730
|
* templates: a deploy upserts them and never deletes one, because profiles
|
|
@@ -10052,7 +10262,7 @@ const journeys = defineModule(defineRoute({
|
|
|
10052
10262
|
responses: {
|
|
10053
10263
|
200: envelope(z.array(journeyWithOfferSchema), "The journeys as they now stand, each with the members turning it on has waiting"),
|
|
10054
10264
|
...sessionErrors,
|
|
10055
|
-
...errors("not_found", "
|
|
10265
|
+
...errors("not_found", "validation_failed", "malformed_request")
|
|
10056
10266
|
}
|
|
10057
10267
|
}), defineRoute({
|
|
10058
10268
|
method: "get",
|
|
@@ -10203,63 +10413,11 @@ const me = defineModule(defineRoute({
|
|
|
10203
10413
|
}
|
|
10204
10414
|
}));
|
|
10205
10415
|
|
|
10206
|
-
//#endregion
|
|
10207
|
-
//#region ../../packages/shared/src/contract/project.ts
|
|
10208
|
-
/**
|
|
10209
|
-
* The org's project. Singular in the path on purpose: a caller always means
|
|
10210
|
-
* one, the one its `cow.json` names, so the name is a query parameter and
|
|
10211
|
-
* there is no id to put in a URL. An org with a single project needs neither.
|
|
10212
|
-
*/
|
|
10213
|
-
const project = defineModule(defineRoute({
|
|
10214
|
-
method: "get",
|
|
10215
|
-
path: "/v1/project",
|
|
10216
|
-
operationId: "project.get",
|
|
10217
|
-
tags: ["project"],
|
|
10218
|
-
summary: "Get one of the organization's projects",
|
|
10219
|
-
security: PIPELINE_AUTH,
|
|
10220
|
-
request: { query: getProjectQuerySchema },
|
|
10221
|
-
responses: {
|
|
10222
|
-
200: envelope(projectSchema),
|
|
10223
|
-
...sessionErrors,
|
|
10224
|
-
...errors("project_missing", "validation_failed", "malformed_request")
|
|
10225
|
-
}
|
|
10226
|
-
}), defineRoute({
|
|
10227
|
-
method: "get",
|
|
10228
|
-
path: "/v1/project/status",
|
|
10229
|
-
operationId: "project.status",
|
|
10230
|
-
tags: ["project"],
|
|
10231
|
-
summary: "What a project's journeys and templates are doing",
|
|
10232
|
-
security: PIPELINE_AUTH,
|
|
10233
|
-
surfaces: {
|
|
10234
|
-
cli: false,
|
|
10235
|
-
mcpName: "status"
|
|
10236
|
-
},
|
|
10237
|
-
request: { query: getProjectQuerySchema },
|
|
10238
|
-
responses: {
|
|
10239
|
-
200: envelope(projectStatusSchema),
|
|
10240
|
-
...sessionErrors,
|
|
10241
|
-
...errors("project_missing", "validation_failed", "malformed_request")
|
|
10242
|
-
}
|
|
10243
|
-
}), defineRoute({
|
|
10244
|
-
method: "post",
|
|
10245
|
-
path: "/v1/project",
|
|
10246
|
-
operationId: "project.create",
|
|
10247
|
-
tags: ["project"],
|
|
10248
|
-
summary: "Create a project in the organization",
|
|
10249
|
-
security: PIPELINE_AUTH,
|
|
10250
|
-
request: { body: jsonBody(createProjectBodySchema) },
|
|
10251
|
-
responses: {
|
|
10252
|
-
201: envelope(projectSchema, "The created project"),
|
|
10253
|
-
...sessionErrors,
|
|
10254
|
-
...errors("project_exists", "validation_failed", "malformed_request")
|
|
10255
|
-
}
|
|
10256
|
-
}));
|
|
10257
|
-
|
|
10258
10416
|
//#endregion
|
|
10259
10417
|
//#region ../../packages/shared/src/contract/pushes.ts
|
|
10260
10418
|
const params$6 = z.object({ id: z.string() });
|
|
10261
10419
|
/**
|
|
10262
|
-
* Pushes: one `cow push` each, the whole
|
|
10420
|
+
* Pushes: one `cow push` each, the whole app at one point in time. The
|
|
10263
10421
|
* push stores the source archive and its compile creates a version of every
|
|
10264
10422
|
* key whose module changed; it turns nothing on (ADR 0011).
|
|
10265
10423
|
*
|
|
@@ -10273,14 +10431,14 @@ const pushes = defineModule(defineRoute({
|
|
|
10273
10431
|
path: "/v1/pushes",
|
|
10274
10432
|
operationId: "pushes.create",
|
|
10275
10433
|
tags: ["pushes"],
|
|
10276
|
-
summary: "Push a built
|
|
10434
|
+
summary: "Push a built app",
|
|
10277
10435
|
security: PIPELINE_AUTH,
|
|
10278
10436
|
surfaces: HIDDEN_FROM_TOOLS,
|
|
10279
10437
|
request: { body: jsonBody(createPushBodySchema) },
|
|
10280
10438
|
responses: {
|
|
10281
10439
|
201: envelope(pushSchema, "The stored push and the versions it is compiling"),
|
|
10282
10440
|
...sessionErrors,
|
|
10283
|
-
...errors("
|
|
10441
|
+
...errors("push_invalid", "validation_failed", "malformed_request", "dependency_unavailable")
|
|
10284
10442
|
}
|
|
10285
10443
|
}), defineRoute({
|
|
10286
10444
|
method: "get",
|
|
@@ -10331,8 +10489,8 @@ const pushes = defineModule(defineRoute({
|
|
|
10331
10489
|
//#region ../../packages/shared/src/contract/resend.ts
|
|
10332
10490
|
/**
|
|
10333
10491
|
* The Resend-compatible send facade (ADR 0014): a developer points
|
|
10334
|
-
* `RESEND_BASE_URL` at `/resend/{
|
|
10335
|
-
*
|
|
10492
|
+
* `RESEND_BASE_URL` at `/resend/{appOrSourceId}` and keeps their code. The
|
|
10493
|
+
* path segment names the app, because the Resend SDK carries no other field
|
|
10336
10494
|
* and accepts no custom header; the bearer is the organization's ingestion
|
|
10337
10495
|
* key.
|
|
10338
10496
|
*
|
|
@@ -10348,7 +10506,15 @@ const FACADE_SURFACES = {
|
|
|
10348
10506
|
mcp: false,
|
|
10349
10507
|
docs: false
|
|
10350
10508
|
};
|
|
10351
|
-
|
|
10509
|
+
/**
|
|
10510
|
+
* The app the message belongs to: its own id, or the id of its one `api`
|
|
10511
|
+
* source, which is what every base URL held before app ids were accepted
|
|
10512
|
+
* here. Both resolve to that source, so attribution is the same either way.
|
|
10513
|
+
*/
|
|
10514
|
+
const targetParam = z.object({ appOrSourceId: z.union([appIdSchema, sourceIdSchema], { error: "Use an app id (\"app_website\") or the id of its API source." }).openapi({
|
|
10515
|
+
example: "app_website",
|
|
10516
|
+
description: "The app to send for, or the id of its API source. Both name the same pipe."
|
|
10517
|
+
}) });
|
|
10352
10518
|
/** Resend's error body, for the answers this facade gives. */
|
|
10353
10519
|
const facadeError = (description) => ({
|
|
10354
10520
|
description,
|
|
@@ -10356,21 +10522,21 @@ const facadeError = (description) => ({
|
|
|
10356
10522
|
});
|
|
10357
10523
|
const facadeErrors = {
|
|
10358
10524
|
401: facadeError("missing_api_key | invalid_api_key"),
|
|
10359
|
-
404: facadeError("not_found: unknown source, or unknown message"),
|
|
10525
|
+
404: facadeError("not_found: unknown app or source, or unknown message"),
|
|
10360
10526
|
422: facadeError("validation_error | missing_required_field"),
|
|
10361
10527
|
429: facadeError("monthly_quota_exceeded | rate_limit_exceeded"),
|
|
10362
10528
|
500: facadeError("application_error")
|
|
10363
10529
|
};
|
|
10364
10530
|
const resend = defineModule(defineRoute({
|
|
10365
10531
|
method: "post",
|
|
10366
|
-
path: "/resend/{
|
|
10532
|
+
path: "/resend/{appOrSourceId}/emails",
|
|
10367
10533
|
operationId: "resend.send",
|
|
10368
10534
|
tags: ["resend"],
|
|
10369
10535
|
summary: "Send one email (Resend-compatible)",
|
|
10370
10536
|
security: API_KEY_AUTH,
|
|
10371
10537
|
surfaces: FACADE_SURFACES,
|
|
10372
10538
|
request: {
|
|
10373
|
-
params:
|
|
10539
|
+
params: targetParam,
|
|
10374
10540
|
body: jsonBody(resendSendBodySchema)
|
|
10375
10541
|
},
|
|
10376
10542
|
responses: {
|
|
@@ -10383,14 +10549,14 @@ const resend = defineModule(defineRoute({
|
|
|
10383
10549
|
}
|
|
10384
10550
|
}), defineRoute({
|
|
10385
10551
|
method: "post",
|
|
10386
|
-
path: "/resend/{
|
|
10552
|
+
path: "/resend/{appOrSourceId}/emails/batch",
|
|
10387
10553
|
operationId: "resend.sendBatch",
|
|
10388
10554
|
tags: ["resend"],
|
|
10389
10555
|
summary: "Send up to 100 emails (Resend-compatible)",
|
|
10390
10556
|
security: API_KEY_AUTH,
|
|
10391
10557
|
surfaces: FACADE_SURFACES,
|
|
10392
10558
|
request: {
|
|
10393
|
-
params:
|
|
10559
|
+
params: targetParam,
|
|
10394
10560
|
body: jsonBody(resendBatchBodySchema)
|
|
10395
10561
|
},
|
|
10396
10562
|
responses: {
|
|
@@ -10403,13 +10569,13 @@ const resend = defineModule(defineRoute({
|
|
|
10403
10569
|
}
|
|
10404
10570
|
}), defineRoute({
|
|
10405
10571
|
method: "get",
|
|
10406
|
-
path: "/resend/{
|
|
10572
|
+
path: "/resend/{appOrSourceId}/emails/{id}",
|
|
10407
10573
|
operationId: "resend.get",
|
|
10408
10574
|
tags: ["resend"],
|
|
10409
10575
|
summary: "Fetch one sent email (Resend-compatible)",
|
|
10410
10576
|
security: API_KEY_AUTH,
|
|
10411
10577
|
surfaces: FACADE_SURFACES,
|
|
10412
|
-
request: { params:
|
|
10578
|
+
request: { params: targetParam.extend({ id: z.string().openapi({ example: "dlv_2f9a8c1b" }) }) },
|
|
10413
10579
|
responses: {
|
|
10414
10580
|
200: {
|
|
10415
10581
|
description: "The email object",
|
|
@@ -10896,8 +11062,18 @@ const mergedRedirect = { 307: {
|
|
|
10896
11062
|
headers: z.object({ Location: z.string().meta({ description: "The survivor's URL" }) })
|
|
10897
11063
|
} };
|
|
10898
11064
|
/**
|
|
10899
|
-
* Profiles by their Cowliss-generated id. `find`
|
|
10900
|
-
* the literal
|
|
11065
|
+
* Profiles by their Cowliss-generated id. `find`, `export` and `erase`
|
|
11066
|
+
* precede `{profileId}` so the literal paths win.
|
|
11067
|
+
*
|
|
11068
|
+
* Compliance has two ways in (ADR 0022). A `usr_` id names one profile, and
|
|
11069
|
+
* erasure or export takes it with everything merged into it. A
|
|
11070
|
+
* `{ kind, value }` identifier names a person, which is one profile per app
|
|
11071
|
+
* that knows the value, and the same erasure or export runs over all of
|
|
11072
|
+
* them at once: the addressing differs, nothing below it does. An identifier
|
|
11073
|
+
* never travels in the URL, neither in the path nor in a query string,
|
|
11074
|
+
* because a GDPR request carries an address and a URL is logged whole. That
|
|
11075
|
+
* is why both identifier routes are a POST carrying a body, including the
|
|
11076
|
+
* export, which reads.
|
|
10901
11077
|
*/
|
|
10902
11078
|
const users = defineModule(defineRoute({
|
|
10903
11079
|
method: "get",
|
|
@@ -10925,6 +11101,32 @@ const users = defineModule(defineRoute({
|
|
|
10925
11101
|
...sessionErrors,
|
|
10926
11102
|
...errors("not_found", "validation_failed")
|
|
10927
11103
|
}
|
|
11104
|
+
}), defineRoute({
|
|
11105
|
+
method: "post",
|
|
11106
|
+
path: "/v1/users/export",
|
|
11107
|
+
operationId: "users.exportByIdentifier",
|
|
11108
|
+
tags: ["users"],
|
|
11109
|
+
summary: "Export everything held on the people one identifier names",
|
|
11110
|
+
security: SESSION_AUTH,
|
|
11111
|
+
request: { body: jsonBody(exportUserByIdentifierBodySchema) },
|
|
11112
|
+
responses: {
|
|
11113
|
+
200: envelope(userExportSchema),
|
|
11114
|
+
...sessionErrors,
|
|
11115
|
+
...errors("not_found", "validation_failed", "malformed_request")
|
|
11116
|
+
}
|
|
11117
|
+
}), defineRoute({
|
|
11118
|
+
method: "post",
|
|
11119
|
+
path: "/v1/users/erase",
|
|
11120
|
+
operationId: "users.eraseByIdentifier",
|
|
11121
|
+
tags: ["users"],
|
|
11122
|
+
summary: "Erase every person one identifier names (GDPR)",
|
|
11123
|
+
security: SESSION_AUTH,
|
|
11124
|
+
request: { body: jsonBody(eraseUserByIdentifierBodySchema) },
|
|
11125
|
+
responses: {
|
|
11126
|
+
200: envelope(eraseUserResponseSchema),
|
|
11127
|
+
...sessionErrors,
|
|
11128
|
+
...errors("not_found", "validation_failed", "malformed_request")
|
|
11129
|
+
}
|
|
10928
11130
|
}), defineRoute({
|
|
10929
11131
|
method: "get",
|
|
10930
11132
|
path: "/v1/users/{profileId}",
|
|
@@ -11188,7 +11390,6 @@ const contract = {
|
|
|
11188
11390
|
violations,
|
|
11189
11391
|
review,
|
|
11190
11392
|
segments,
|
|
11191
|
-
project,
|
|
11192
11393
|
artifacts,
|
|
11193
11394
|
pushes,
|
|
11194
11395
|
versions,
|
|
@@ -11581,7 +11782,7 @@ function registerContractCommands(program, run) {
|
|
|
11581
11782
|
*/
|
|
11582
11783
|
/**
|
|
11583
11784
|
* One call, whichever way the keys were named: the route takes the set, and
|
|
11584
|
-
* `--all` is the
|
|
11785
|
+
* `--all` is the app id, resolved on the server against the rows the app
|
|
11585
11786
|
* owns (so it reaches a key whose file the tree lost). A key that is not
|
|
11586
11787
|
* there refuses the whole call, and nothing changes.
|
|
11587
11788
|
*/
|
|
@@ -11620,7 +11821,7 @@ const STATE_WORD = {
|
|
|
11620
11821
|
};
|
|
11621
11822
|
/** What the terminal says: one line per key, naming its new state. */
|
|
11622
11823
|
function flipSummary(journeys, status) {
|
|
11623
|
-
if (journeys.length === 0) return `This
|
|
11824
|
+
if (journeys.length === 0) return `This app has no journeys to put ${STATE_WORD[status]}.`;
|
|
11624
11825
|
return journeys.map((journey) => [`${journey.key} is ${STATE_WORD[status]}.`, ...segmentLines(journey)].join("\n")).join("\n");
|
|
11625
11826
|
}
|
|
11626
11827
|
const DESCRIPTIONS = {
|
|
@@ -11629,14 +11830,14 @@ const DESCRIPTIONS = {
|
|
|
11629
11830
|
paused: "hold journeys: nobody enters, and everyone part-way through waits"
|
|
11630
11831
|
};
|
|
11631
11832
|
function register(program, clientFor, io, verb, status) {
|
|
11632
|
-
program.command(verb).description(`${DESCRIPTIONS[status]} (many keys, or --all for this
|
|
11833
|
+
program.command(verb).description(`${DESCRIPTIONS[status]} (many keys, or --all for this app's)`).argument("[keys...]", "journey keys").option("--all", "every journey of this app").action(async (keys, opts) => {
|
|
11633
11834
|
const merged = {
|
|
11634
11835
|
...program.opts(),
|
|
11635
11836
|
...opts
|
|
11636
11837
|
};
|
|
11637
11838
|
if (merged.all === true && keys.length > 0) throw new Error(`Name the journeys or pass --all, not both: cow ${verb} welcome.`);
|
|
11638
11839
|
if (merged.all !== true && keys.length === 0) throw new Error(`Name at least one journey, or pass --all: cow ${verb} welcome.`);
|
|
11639
|
-
const selector = merged.all === true ? {
|
|
11840
|
+
const selector = merged.all === true ? { appId: (await assertCowConfig(process.cwd())).appId } : { keys };
|
|
11640
11841
|
const updated = await setStatus(await clientFor(merged), selector, status);
|
|
11641
11842
|
if (merged.json === true) {
|
|
11642
11843
|
emit({ data: updated }, io, true);
|
|
@@ -11696,43 +11897,45 @@ async function packageJson(name) {
|
|
|
11696
11897
|
}
|
|
11697
11898
|
});
|
|
11698
11899
|
}
|
|
11900
|
+
/** The org's active apps, or null when this machine cannot read them. */
|
|
11901
|
+
async function listApps(client) {
|
|
11902
|
+
try {
|
|
11903
|
+
return (await client.request(contract.apps["apps.list"], { query: {
|
|
11904
|
+
status: "active",
|
|
11905
|
+
limit: 100
|
|
11906
|
+
} })).data.map((app) => app.id);
|
|
11907
|
+
} catch {
|
|
11908
|
+
return null;
|
|
11909
|
+
}
|
|
11910
|
+
}
|
|
11699
11911
|
/**
|
|
11700
|
-
*
|
|
11701
|
-
*
|
|
11702
|
-
*
|
|
11703
|
-
*
|
|
11704
|
-
* ponytail: plain fetch because `POST /v1/project` is not a contract route
|
|
11705
|
-
* yet; swap it for the typed client's `project.create` once ticket 04 lands.
|
|
11912
|
+
* Pick the app this repository pushes to. One app is the answer; several
|
|
11913
|
+
* are a question; none is a refusal, because the answer is in the dashboard
|
|
11914
|
+
* and `cow init` is not allowed to invent one.
|
|
11706
11915
|
*/
|
|
11707
|
-
async function
|
|
11708
|
-
if (
|
|
11709
|
-
|
|
11710
|
-
|
|
11916
|
+
async function chooseApp(client, webUrl, flag, org, io, skipPrompts) {
|
|
11917
|
+
if (flag) return { appId: flag };
|
|
11918
|
+
if (org.claimed !== null && org.claimed !== org.requested) return {
|
|
11919
|
+
appId: "",
|
|
11920
|
+
reason: `this machine is logged in to ${org.claimed}, not ${org.requested}`
|
|
11711
11921
|
};
|
|
11712
|
-
|
|
11713
|
-
|
|
11714
|
-
|
|
11715
|
-
|
|
11716
|
-
|
|
11717
|
-
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
11725
|
-
|
|
11726
|
-
};
|
|
11727
|
-
} catch (error) {
|
|
11728
|
-
return {
|
|
11729
|
-
project: "skipped",
|
|
11730
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
11731
|
-
};
|
|
11732
|
-
}
|
|
11922
|
+
const apps = await listApps(client);
|
|
11923
|
+
if (apps === null) return {
|
|
11924
|
+
appId: "",
|
|
11925
|
+
reason: "this machine could not read the org's apps"
|
|
11926
|
+
};
|
|
11927
|
+
if (apps.length === 0) throw new Error(`This organization has no app yet, and a repository pushes to one. Create one at ${webUrl}/apps, then run \`cow init\` again.`);
|
|
11928
|
+
const only = apps[0];
|
|
11929
|
+
if (apps.length === 1 && only) return { appId: only };
|
|
11930
|
+
if (skipPrompts || !io.isTTY) return {
|
|
11931
|
+
appId: "",
|
|
11932
|
+
reason: `this organization has several apps (${apps.join(", ")}) and nothing said which`
|
|
11933
|
+
};
|
|
11934
|
+
io.stderr(`Apps in this organization:\n${apps.map((id) => ` ${id}`).join("\n")}\n`);
|
|
11935
|
+
return { appId: await ask("App id", only ?? "", io, false) };
|
|
11733
11936
|
}
|
|
11734
|
-
function registerInit(program, env, io) {
|
|
11735
|
-
program.command("init").argument("[dir]", "directory to create the
|
|
11937
|
+
function registerInit(program, clientFor, env, io) {
|
|
11938
|
+
program.command("init").argument("[dir]", "directory to create the repo in (default: here)").description("create a cow repo: cow.json, journeys/, emails/, and the files to build them").option("--org <id>", "organization id (default: the logged-in org)").option("--app <id>", "app id this repository pushes to (default: ask, or the org's only one)").option("--yes", "accept the defaults instead of prompting").option("--example <name>", "start from a gallery example").action(async (dirArg, opts) => {
|
|
11736
11939
|
const merged = {
|
|
11737
11940
|
...program.opts(),
|
|
11738
11941
|
...opts
|
|
@@ -11740,17 +11943,27 @@ function registerInit(program, env, io) {
|
|
|
11740
11943
|
const skipPrompts = opts.yes === true;
|
|
11741
11944
|
const cwd = process.cwd();
|
|
11742
11945
|
const defaultName = basename(dirArg ? resolve(cwd, dirArg) : cwd);
|
|
11743
|
-
const name = await ask("
|
|
11946
|
+
const name = await ask("Repo name", defaultName, io, skipPrompts);
|
|
11744
11947
|
const dir = dirArg ? resolve(cwd, dirArg) : name === defaultName ? cwd : resolve(cwd, name);
|
|
11745
11948
|
if (existsSync(join(dir, "cow.json"))) throw new Error(`"${dir}" already holds a cow.json. Use \`cow add example\` to add code to it.`);
|
|
11746
11949
|
const credentials = await readCredentials(env);
|
|
11747
11950
|
const claimedOrg = credentials ? decodeSessionToken(credentials.token)?.orgId ?? null : null;
|
|
11748
11951
|
const orgId = typeof opts.org === "string" && opts.org !== "" ? opts.org : await ask("Organization id", claimedOrg ?? "", io, skipPrompts);
|
|
11749
11952
|
if (orgId === "") throw new Error("No organization id. Pass --org org_… or run `cow login` first.");
|
|
11750
|
-
|
|
11953
|
+
if (!orgIdSchema.safeParse(orgId).success) throw new Error(`"${orgId}" is not an organization id. They look like org_…; \`cow login\` writes the one you are signed in to.`);
|
|
11954
|
+
const webUrl = resolveWebUrl(env);
|
|
11955
|
+
const app = await chooseApp(await clientFor(merged), webUrl, typeof opts.app === "string" && opts.app !== "" ? opts.app : void 0, {
|
|
11956
|
+
requested: orgId,
|
|
11957
|
+
claimed: claimedOrg
|
|
11958
|
+
}, io, skipPrompts);
|
|
11959
|
+
const config = app.appId === "" ? {
|
|
11751
11960
|
$schema: COW_CONFIG_SCHEMA_URL,
|
|
11752
11961
|
orgId,
|
|
11753
|
-
|
|
11962
|
+
appId: ""
|
|
11963
|
+
} : cowConfigSchema.parse({
|
|
11964
|
+
$schema: COW_CONFIG_SCHEMA_URL,
|
|
11965
|
+
orgId,
|
|
11966
|
+
appId: app.appId
|
|
11754
11967
|
});
|
|
11755
11968
|
const contents = {
|
|
11756
11969
|
"cow.json": json(config),
|
|
@@ -11773,19 +11986,18 @@ function registerInit(program, env, io) {
|
|
|
11773
11986
|
}
|
|
11774
11987
|
const example = typeof opts.example === "string" ? opts.example : void 0;
|
|
11775
11988
|
const files = [...Object.keys(contents), ...example ? await copyExample(example, dir, false) : []].sort();
|
|
11776
|
-
const outcome = await createProject(resolveApiUrl(env, credentials, typeof merged.api === "string" ? merged.api : void 0), resolveCredential(env, credentials).token, name);
|
|
11777
11989
|
emit({ data: {
|
|
11778
11990
|
dir,
|
|
11779
11991
|
orgId,
|
|
11780
11992
|
files,
|
|
11781
|
-
...
|
|
11993
|
+
...app
|
|
11782
11994
|
} }, io, merged.json);
|
|
11783
11995
|
const here = relative(cwd, dir) || ".";
|
|
11784
11996
|
const scenario = files.find((file) => file.startsWith("scenarios/")) ?? "scenarios/abandoned-checkout.timeout.json";
|
|
11785
|
-
const
|
|
11997
|
+
const appLine = app.appId === "" ? `No app named yet (${app.reason}). Put its id in cow.json's "appId"; you will find it on the app's page at ${webUrl}/apps.` : `Pushes to ${app.appId}`;
|
|
11786
11998
|
io.stderr([
|
|
11787
|
-
`Created a cow
|
|
11788
|
-
|
|
11999
|
+
`Created a cow repo in ${here}`,
|
|
12000
|
+
appLine,
|
|
11789
12001
|
"",
|
|
11790
12002
|
"Next steps:",
|
|
11791
12003
|
` cd ${here}`,
|
|
@@ -11959,25 +12171,25 @@ function noGitHere(error) {
|
|
|
11959
12171
|
return failure.code === "ENOENT" || String(failure.stderr ?? "").includes("not a git repository");
|
|
11960
12172
|
}
|
|
11961
12173
|
/**
|
|
11962
|
-
* The push to restore: the one named, else this
|
|
11963
|
-
*
|
|
11964
|
-
*
|
|
12174
|
+
* The push to restore: the one named, else this app's newest. Scoped to this
|
|
12175
|
+
* directory's app, so a sibling repo's tree is never unpacked over this
|
|
12176
|
+
* one's files.
|
|
11965
12177
|
*
|
|
11966
12178
|
* The newest rather than the newest that compiled: a push stores the whole
|
|
11967
12179
|
* tree and its source archive whatever its keys then compiled to, so it is
|
|
11968
12180
|
* the last thing anyone pushed either way.
|
|
11969
12181
|
*/
|
|
11970
|
-
async function resolvePush(client, pushId,
|
|
12182
|
+
async function resolvePush(client, pushId, appId) {
|
|
11971
12183
|
if (pushId) return (await client.request(contract.pushes["pushes.get"], { params: { id: pushId } })).data;
|
|
11972
12184
|
const newest = (await client.request(contract.pushes["pushes.list"], { query: {
|
|
11973
12185
|
limit: 1,
|
|
11974
|
-
|
|
12186
|
+
appId
|
|
11975
12187
|
} })).data[0];
|
|
11976
|
-
if (!newest) throw new Error("This
|
|
12188
|
+
if (!newest) throw new Error("This app has never been pushed, so there is nothing to restore. Run `cow push` first, or name a push with --push.");
|
|
11977
12189
|
return newest;
|
|
11978
12190
|
}
|
|
11979
12191
|
async function pullPush({ client, projectDir, pushId, force }) {
|
|
11980
|
-
const push = await resolvePush(client, pushId, await
|
|
12192
|
+
const push = await resolvePush(client, pushId, await configuredApp(projectDir));
|
|
11981
12193
|
if (!force) {
|
|
11982
12194
|
const dirty = await dirtyPaths(projectDir);
|
|
11983
12195
|
if (dirty.length > 0) throw new Error(`This working tree has uncommitted changes and \`cow pull\` would write over them:\n ${dirty.join("\n ")}\nCommit or stash them, or pass --force.`);
|
|
@@ -11999,7 +12211,7 @@ async function pullPush({ client, projectDir, pushId, force }) {
|
|
|
11999
12211
|
};
|
|
12000
12212
|
}
|
|
12001
12213
|
function registerPull(program, clientFor, io) {
|
|
12002
|
-
program.command("pull").description("restore the
|
|
12214
|
+
program.command("pull").description("restore the repo's source files from a push into the current directory").option("--push <id>", "the push to restore (default: the newest one)").option("--force", "extract even when the git working tree is dirty").action(async (opts) => {
|
|
12003
12215
|
const merged = {
|
|
12004
12216
|
...program.opts(),
|
|
12005
12217
|
...opts
|
|
@@ -12020,25 +12232,6 @@ function registerPull(program, clientFor, io) {
|
|
|
12020
12232
|
|
|
12021
12233
|
//#endregion
|
|
12022
12234
|
//#region src/commands/push.ts
|
|
12023
|
-
/**
|
|
12024
|
-
* This directory's project, created when the org has none by that name.
|
|
12025
|
-
* `cow init` usually got there first; a project pushed from a machine that
|
|
12026
|
-
* only ever cloned the repo has not, and creating it here is what makes that
|
|
12027
|
-
* clone work. The name is `cow.json`'s `project`.
|
|
12028
|
-
*/
|
|
12029
|
-
async function ensureProject(client, name) {
|
|
12030
|
-
try {
|
|
12031
|
-
return (await client.request(contract.project["project.get"], { query: { name } })).data;
|
|
12032
|
-
} catch (error) {
|
|
12033
|
-
if (!(error instanceof ApiError) || error.code !== "project_missing") throw error;
|
|
12034
|
-
}
|
|
12035
|
-
try {
|
|
12036
|
-
return (await client.request(contract.project["project.create"], { body: { name } })).data;
|
|
12037
|
-
} catch (error) {
|
|
12038
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
12039
|
-
throw new Error(`This organization has no project and one could not be created (${reason}). Run \`cow init\` first.`);
|
|
12040
|
-
}
|
|
12041
|
-
}
|
|
12042
12235
|
/** Does the org already hold these bytes? A 404 is the answer, not a failure. */
|
|
12043
12236
|
async function held(client, digest) {
|
|
12044
12237
|
try {
|
|
@@ -12072,8 +12265,6 @@ function artifactFiles(projectDir, manifest) {
|
|
|
12072
12265
|
}
|
|
12073
12266
|
async function pushProject({ client, projectDir }) {
|
|
12074
12267
|
const { manifest, config } = await buildProject(projectDir);
|
|
12075
|
-
const project = await ensureProject(client, config.project);
|
|
12076
|
-
if (project.orgId !== config.orgId) throw new Error(`cow.json is for ${config.orgId} but the credential belongs to ${project.orgId}. Log in to that organization, use its pipeline key, or select a config for it with --config or COW_CONFIG (for example cow.local.json).`);
|
|
12077
12268
|
const uploaded = [];
|
|
12078
12269
|
for (const artifact of artifactFiles(projectDir, manifest)) {
|
|
12079
12270
|
if (await held(client, artifact.digest)) continue;
|
|
@@ -12090,7 +12281,8 @@ async function pushProject({ client, projectDir }) {
|
|
|
12090
12281
|
...manifest,
|
|
12091
12282
|
protocol: 3
|
|
12092
12283
|
},
|
|
12093
|
-
|
|
12284
|
+
appId: config.appId,
|
|
12285
|
+
orgId: config.orgId
|
|
12094
12286
|
} })).data,
|
|
12095
12287
|
uploaded
|
|
12096
12288
|
};
|
|
@@ -12106,10 +12298,10 @@ function pushSummary(outcome, webUrl) {
|
|
|
12106
12298
|
const stored = `Push #${push.seq} stored (${uploaded}).`;
|
|
12107
12299
|
if (push.versions.length === 0) return `${stored} Nothing changed.`;
|
|
12108
12300
|
const width = Math.max(...push.versions.map((version) => version.key.length));
|
|
12109
|
-
return [`${stored} Compiling ${push.versions.length} ${push.versions.length === 1 ? "key" : "keys"}:`, ...push.versions.map((version) => ` ${version.key.padEnd(width)} ${webUrl}/${version.kind === "journey" ? "journeys" : "templates"}/${version.key}`)].join("\n");
|
|
12301
|
+
return [`${stored} Compiling ${push.versions.length} ${push.versions.length === 1 ? "key" : "keys"}:`, ...push.versions.map((version) => ` ${version.key.padEnd(width)} ${webUrl}/${version.kind === "journey" ? "journeys" : "deliveries/templates"}/${version.key}`)].join("\n");
|
|
12110
12302
|
}
|
|
12111
12303
|
function registerPush(program, clientFor, env, io) {
|
|
12112
|
-
program.command("push").description("build
|
|
12304
|
+
program.command("push").description("build this repository and upload it; the server compiles what changed").action(async (opts) => {
|
|
12113
12305
|
const merged = {
|
|
12114
12306
|
...program.opts(),
|
|
12115
12307
|
...opts
|
|
@@ -12130,10 +12322,10 @@ function registerPush(program, clientFor, env, io) {
|
|
|
12130
12322
|
//#endregion
|
|
12131
12323
|
//#region src/commands/status.ts
|
|
12132
12324
|
/**
|
|
12133
|
-
* `cow status`: what the server holds for every key of this
|
|
12134
|
-
*
|
|
12135
|
-
*
|
|
12136
|
-
*
|
|
12325
|
+
* `cow status`: what the server holds for every key of this app, and how far
|
|
12326
|
+
* the tree in front of the developer has moved from it. Hand-written rather
|
|
12327
|
+
* than derived from `apps.status`, because the drift is the half of the
|
|
12328
|
+
* answer no server read can know: it comes from building the repository
|
|
12137
12329
|
* here and comparing the bundle digests with the versions the server has.
|
|
12138
12330
|
*/
|
|
12139
12331
|
/** Largest first: the first unit the gap fills is the one that reads best. */
|
|
@@ -12207,7 +12399,7 @@ function statusLines(status, manifest, now) {
|
|
|
12207
12399
|
const local = manifest ? localKeys(manifest) : [];
|
|
12208
12400
|
const journeys = status.keys.filter((entry) => entry.kind === "journey");
|
|
12209
12401
|
const templates = status.keys.filter((entry) => entry.kind === "template");
|
|
12210
|
-
const lines = [`${status.
|
|
12402
|
+
const lines = [`${status.appId}: ${journeys.length} ${journeys.length === 1 ? "journey" : "journeys"}, ${templates.length} ${templates.length === 1 ? "template" : "templates"} on the server.`];
|
|
12211
12403
|
const width = Math.max(0, ...status.keys.map((entry) => entry.key.length));
|
|
12212
12404
|
const section = (title, entries) => {
|
|
12213
12405
|
if (entries.length === 0) return;
|
|
@@ -12246,19 +12438,19 @@ function statusLines(status, manifest, now) {
|
|
|
12246
12438
|
}
|
|
12247
12439
|
return lines;
|
|
12248
12440
|
}
|
|
12249
|
-
/** The
|
|
12250
|
-
async function readStatus(client,
|
|
12251
|
-
return (await client.request(contract.
|
|
12441
|
+
/** The app's status from the server, for this directory's `cow.json`. */
|
|
12442
|
+
async function readStatus(client, appId) {
|
|
12443
|
+
return (await client.request(contract.apps["apps.status"], { params: { id: appId } })).data;
|
|
12252
12444
|
}
|
|
12253
12445
|
function registerStatus(program, clientFor, io) {
|
|
12254
|
-
program.command("status").description("what this
|
|
12446
|
+
program.command("status").description("what this app's journeys and templates are doing, and what changed here since the last push").action(async (opts) => {
|
|
12255
12447
|
const merged = {
|
|
12256
12448
|
...program.opts(),
|
|
12257
12449
|
...opts
|
|
12258
12450
|
};
|
|
12259
12451
|
const config = await assertCowConfig(process.cwd());
|
|
12260
12452
|
const built = await buildProject(process.cwd()).catch(() => null);
|
|
12261
|
-
const status = await readStatus(await clientFor(merged), config.
|
|
12453
|
+
const status = await readStatus(await clientFor(merged), config.appId);
|
|
12262
12454
|
if (merged.json === true) {
|
|
12263
12455
|
emit({ data: status }, io, true);
|
|
12264
12456
|
return;
|
|
@@ -12527,7 +12719,7 @@ function emit(result, io, json) {
|
|
|
12527
12719
|
}
|
|
12528
12720
|
function buildProgram(env, io) {
|
|
12529
12721
|
const program = new Command();
|
|
12530
|
-
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>", `
|
|
12722
|
+
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>", `config file to read instead of ${DEFAULT_CONFIG_FILE} (overrides COW_CONFIG)`);
|
|
12531
12723
|
program.hook("preAction", () => {
|
|
12532
12724
|
const flag = program.opts().config;
|
|
12533
12725
|
setConfigFile((typeof flag === "string" ? flag : void 0) ?? env.COW_CONFIG ?? "cow.json");
|
|
@@ -12549,7 +12741,7 @@ function buildProgram(env, io) {
|
|
|
12549
12741
|
};
|
|
12550
12742
|
registerContractCommands(program, run);
|
|
12551
12743
|
registerAuth(program, env, io);
|
|
12552
|
-
registerInit(program, env, io);
|
|
12744
|
+
registerInit(program, clientFor, env, io);
|
|
12553
12745
|
registerAdd(program, io);
|
|
12554
12746
|
registerBuild(program, io);
|
|
12555
12747
|
registerPush(program, clientFor, env, io);
|