@cowliss/cli 0.7.0 → 0.8.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-C1bsCjZT.js → driver-BXuYP007.js} +1 -1
- package/dist/guest/driver.d.ts +1 -1
- package/dist/guest/driver.js +1 -1
- 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.js +1 -1
- package/dist/index.js +527 -349
- package/package.json +1 -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),
|
|
@@ -8278,7 +8445,9 @@ async function assertCowConfig(projectDir) {
|
|
|
8278
8445
|
} catch {
|
|
8279
8446
|
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
8447
|
}
|
|
8281
|
-
const
|
|
8448
|
+
const raw = JSON.parse(text);
|
|
8449
|
+
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.`);
|
|
8450
|
+
const parsed = cowConfigSchema.safeParse(raw);
|
|
8282
8451
|
if (!parsed.success) throw new Error(`${name} is invalid: ${parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ")}`);
|
|
8283
8452
|
return parsed.data;
|
|
8284
8453
|
}
|
|
@@ -8291,14 +8460,14 @@ async function readCowConfig(projectDir) {
|
|
|
8291
8460
|
}
|
|
8292
8461
|
}
|
|
8293
8462
|
/**
|
|
8294
|
-
* Which
|
|
8463
|
+
* Which app of the org this directory pushes to: the `appId` in its
|
|
8295
8464
|
* `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
|
-
*
|
|
8465
|
+
* undefined case is `cow pull` into an empty directory, which has no app to
|
|
8466
|
+
* scope by yet and reads the org's pushes.
|
|
8298
8467
|
*/
|
|
8299
|
-
async function
|
|
8468
|
+
async function configuredApp(projectDir) {
|
|
8300
8469
|
try {
|
|
8301
|
-
return (await assertCowConfig(projectDir)).
|
|
8470
|
+
return (await assertCowConfig(projectDir)).appId;
|
|
8302
8471
|
} catch {
|
|
8303
8472
|
return;
|
|
8304
8473
|
}
|
|
@@ -8569,7 +8738,7 @@ async function copyExample(name, dir, force) {
|
|
|
8569
8738
|
return files;
|
|
8570
8739
|
}
|
|
8571
8740
|
function registerAdd(program, io) {
|
|
8572
|
-
program.command("add").description("add gallery example code to a cow
|
|
8741
|
+
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
8742
|
const dir = process.cwd();
|
|
8574
8743
|
await assertCowConfig(dir);
|
|
8575
8744
|
const files = await copyExample(name, dir, opts.force === true);
|
|
@@ -8598,10 +8767,13 @@ async function login(env, options) {
|
|
|
8598
8767
|
const claims = decodeSessionToken(token);
|
|
8599
8768
|
if (claims === null) throw new Error("Token is not a decodable JWT");
|
|
8600
8769
|
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;
|
|
8770
|
+
const apiUrl = options.apiFlag ?? resolveApiUrl(env, null, void 0, (await readCowConfig(process.cwd()))?.apiUrl);
|
|
8603
8771
|
return {
|
|
8604
|
-
credentialsPath: await writeCredentials(env,
|
|
8772
|
+
credentialsPath: await writeCredentials(env, {
|
|
8773
|
+
token,
|
|
8774
|
+
apiUrl
|
|
8775
|
+
}),
|
|
8776
|
+
apiUrl,
|
|
8605
8777
|
userId: claims.userId,
|
|
8606
8778
|
orgId: claims.orgId,
|
|
8607
8779
|
role: claims.role,
|
|
@@ -8688,7 +8860,7 @@ function openBrowser(url) {
|
|
|
8688
8860
|
//#region src/commands/auth.ts
|
|
8689
8861
|
/** login/logout/whoami: session-token management, no contract route. */
|
|
8690
8862
|
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
|
|
8863
|
+
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
8864
|
const outcome = await login(env, {
|
|
8693
8865
|
token: opts.token,
|
|
8694
8866
|
webUrl: opts.web,
|
|
@@ -8718,8 +8890,14 @@ function registerAuth(program, env, io) {
|
|
|
8718
8890
|
}
|
|
8719
8891
|
const claims = decodeSessionToken(credentials?.token ?? "");
|
|
8720
8892
|
if (!claims) throw new Error("Cached token is not decodable. Run `cow login` again.");
|
|
8893
|
+
const projectApiUrl = (await readCowConfig(process.cwd()))?.apiUrl;
|
|
8894
|
+
const apiUrl = resolveApiUrl(env, credentials, opts.api, projectApiUrl);
|
|
8895
|
+
const mintedFor = credentials?.apiUrl;
|
|
8721
8896
|
emit({ data: {
|
|
8722
8897
|
credential: kind,
|
|
8898
|
+
apiUrl,
|
|
8899
|
+
mintedFor: mintedFor ?? null,
|
|
8900
|
+
...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
8901
|
...claims
|
|
8724
8902
|
} }, io, opts.json);
|
|
8725
8903
|
});
|
|
@@ -8732,7 +8910,7 @@ function registerAuth(program, env, io) {
|
|
|
8732
8910
|
* the current directory. Local only, no API call and no network.
|
|
8733
8911
|
*/
|
|
8734
8912
|
function registerBuild(program, io) {
|
|
8735
|
-
program.command("build").description("typecheck and bundle the
|
|
8913
|
+
program.command("build").description("typecheck and bundle the repo in the current directory, writing .cow/build (local: no API call)").action(async (opts) => {
|
|
8736
8914
|
emit({ data: await buildProject(process.cwd()) }, io, opts.json);
|
|
8737
8915
|
});
|
|
8738
8916
|
}
|
|
@@ -9165,7 +9343,8 @@ function htmlResponse(description) {
|
|
|
9165
9343
|
const params$11 = z.object({ id: z.string() });
|
|
9166
9344
|
/**
|
|
9167
9345
|
* An app is the attribution unit: what a developer names, what ingestion
|
|
9168
|
-
* stamps, and what
|
|
9346
|
+
* stamps, and what a profile, a segment and a journey belong to (ADR 0022).
|
|
9347
|
+
* It is also the push unit, which is what `status` reports on. Its inbound
|
|
9169
9348
|
* pipes live under /v1/apps/{appId}/sources, served by the sources module.
|
|
9170
9349
|
*/
|
|
9171
9350
|
const apps = defineModule(defineRoute({
|
|
@@ -9223,6 +9402,23 @@ const apps = defineModule(defineRoute({
|
|
|
9223
9402
|
...sessionErrors,
|
|
9224
9403
|
...errors("not_found", "validation_failed", "malformed_request")
|
|
9225
9404
|
}
|
|
9405
|
+
}), defineRoute({
|
|
9406
|
+
method: "get",
|
|
9407
|
+
path: "/v1/apps/{id}/status",
|
|
9408
|
+
operationId: "apps.status",
|
|
9409
|
+
tags: ["apps"],
|
|
9410
|
+
summary: "What an app's journeys and templates are doing",
|
|
9411
|
+
security: PIPELINE_AUTH,
|
|
9412
|
+
surfaces: {
|
|
9413
|
+
cli: false,
|
|
9414
|
+
mcpName: "status"
|
|
9415
|
+
},
|
|
9416
|
+
request: { params: params$11 },
|
|
9417
|
+
responses: {
|
|
9418
|
+
200: envelope(appStatusSchema),
|
|
9419
|
+
...sessionErrors,
|
|
9420
|
+
...errors("not_found", "validation_failed")
|
|
9421
|
+
}
|
|
9226
9422
|
}));
|
|
9227
9423
|
|
|
9228
9424
|
//#endregion
|
|
@@ -9514,7 +9710,7 @@ const catalog = defineModule(defineRoute({
|
|
|
9514
9710
|
//#region ../../packages/shared/src/contract/consent.ts
|
|
9515
9711
|
/**
|
|
9516
9712
|
* The consent purposes an organization's profiles answer: the two fixed ones
|
|
9517
|
-
* every org has, plus whatever its
|
|
9713
|
+
* every org has, plus whatever its apps declare in their `cow.json`.
|
|
9518
9714
|
*
|
|
9519
9715
|
* Read-only, and deliberately so. Purposes are code, like journeys and
|
|
9520
9716
|
* templates: a deploy upserts them and never deletes one, because profiles
|
|
@@ -10052,7 +10248,7 @@ const journeys = defineModule(defineRoute({
|
|
|
10052
10248
|
responses: {
|
|
10053
10249
|
200: envelope(z.array(journeyWithOfferSchema), "The journeys as they now stand, each with the members turning it on has waiting"),
|
|
10054
10250
|
...sessionErrors,
|
|
10055
|
-
...errors("not_found", "
|
|
10251
|
+
...errors("not_found", "validation_failed", "malformed_request")
|
|
10056
10252
|
}
|
|
10057
10253
|
}), defineRoute({
|
|
10058
10254
|
method: "get",
|
|
@@ -10203,63 +10399,11 @@ const me = defineModule(defineRoute({
|
|
|
10203
10399
|
}
|
|
10204
10400
|
}));
|
|
10205
10401
|
|
|
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
10402
|
//#endregion
|
|
10259
10403
|
//#region ../../packages/shared/src/contract/pushes.ts
|
|
10260
10404
|
const params$6 = z.object({ id: z.string() });
|
|
10261
10405
|
/**
|
|
10262
|
-
* Pushes: one `cow push` each, the whole
|
|
10406
|
+
* Pushes: one `cow push` each, the whole app at one point in time. The
|
|
10263
10407
|
* push stores the source archive and its compile creates a version of every
|
|
10264
10408
|
* key whose module changed; it turns nothing on (ADR 0011).
|
|
10265
10409
|
*
|
|
@@ -10273,14 +10417,14 @@ const pushes = defineModule(defineRoute({
|
|
|
10273
10417
|
path: "/v1/pushes",
|
|
10274
10418
|
operationId: "pushes.create",
|
|
10275
10419
|
tags: ["pushes"],
|
|
10276
|
-
summary: "Push a built
|
|
10420
|
+
summary: "Push a built app",
|
|
10277
10421
|
security: PIPELINE_AUTH,
|
|
10278
10422
|
surfaces: HIDDEN_FROM_TOOLS,
|
|
10279
10423
|
request: { body: jsonBody(createPushBodySchema) },
|
|
10280
10424
|
responses: {
|
|
10281
10425
|
201: envelope(pushSchema, "The stored push and the versions it is compiling"),
|
|
10282
10426
|
...sessionErrors,
|
|
10283
|
-
...errors("
|
|
10427
|
+
...errors("push_invalid", "validation_failed", "malformed_request", "dependency_unavailable")
|
|
10284
10428
|
}
|
|
10285
10429
|
}), defineRoute({
|
|
10286
10430
|
method: "get",
|
|
@@ -10331,8 +10475,8 @@ const pushes = defineModule(defineRoute({
|
|
|
10331
10475
|
//#region ../../packages/shared/src/contract/resend.ts
|
|
10332
10476
|
/**
|
|
10333
10477
|
* The Resend-compatible send facade (ADR 0014): a developer points
|
|
10334
|
-
* `RESEND_BASE_URL` at `/resend/{
|
|
10335
|
-
*
|
|
10478
|
+
* `RESEND_BASE_URL` at `/resend/{appOrSourceId}` and keeps their code. The
|
|
10479
|
+
* path segment names the app, because the Resend SDK carries no other field
|
|
10336
10480
|
* and accepts no custom header; the bearer is the organization's ingestion
|
|
10337
10481
|
* key.
|
|
10338
10482
|
*
|
|
@@ -10348,7 +10492,15 @@ const FACADE_SURFACES = {
|
|
|
10348
10492
|
mcp: false,
|
|
10349
10493
|
docs: false
|
|
10350
10494
|
};
|
|
10351
|
-
|
|
10495
|
+
/**
|
|
10496
|
+
* The app the message belongs to: its own id, or the id of its one `api`
|
|
10497
|
+
* source, which is what every base URL held before app ids were accepted
|
|
10498
|
+
* here. Both resolve to that source, so attribution is the same either way.
|
|
10499
|
+
*/
|
|
10500
|
+
const targetParam = z.object({ appOrSourceId: z.union([appIdSchema, sourceIdSchema], { error: "Use an app id (\"app_website\") or the id of its API source." }).openapi({
|
|
10501
|
+
example: "app_website",
|
|
10502
|
+
description: "The app to send for, or the id of its API source. Both name the same pipe."
|
|
10503
|
+
}) });
|
|
10352
10504
|
/** Resend's error body, for the answers this facade gives. */
|
|
10353
10505
|
const facadeError = (description) => ({
|
|
10354
10506
|
description,
|
|
@@ -10356,21 +10508,21 @@ const facadeError = (description) => ({
|
|
|
10356
10508
|
});
|
|
10357
10509
|
const facadeErrors = {
|
|
10358
10510
|
401: facadeError("missing_api_key | invalid_api_key"),
|
|
10359
|
-
404: facadeError("not_found: unknown source, or unknown message"),
|
|
10511
|
+
404: facadeError("not_found: unknown app or source, or unknown message"),
|
|
10360
10512
|
422: facadeError("validation_error | missing_required_field"),
|
|
10361
10513
|
429: facadeError("monthly_quota_exceeded | rate_limit_exceeded"),
|
|
10362
10514
|
500: facadeError("application_error")
|
|
10363
10515
|
};
|
|
10364
10516
|
const resend = defineModule(defineRoute({
|
|
10365
10517
|
method: "post",
|
|
10366
|
-
path: "/resend/{
|
|
10518
|
+
path: "/resend/{appOrSourceId}/emails",
|
|
10367
10519
|
operationId: "resend.send",
|
|
10368
10520
|
tags: ["resend"],
|
|
10369
10521
|
summary: "Send one email (Resend-compatible)",
|
|
10370
10522
|
security: API_KEY_AUTH,
|
|
10371
10523
|
surfaces: FACADE_SURFACES,
|
|
10372
10524
|
request: {
|
|
10373
|
-
params:
|
|
10525
|
+
params: targetParam,
|
|
10374
10526
|
body: jsonBody(resendSendBodySchema)
|
|
10375
10527
|
},
|
|
10376
10528
|
responses: {
|
|
@@ -10383,14 +10535,14 @@ const resend = defineModule(defineRoute({
|
|
|
10383
10535
|
}
|
|
10384
10536
|
}), defineRoute({
|
|
10385
10537
|
method: "post",
|
|
10386
|
-
path: "/resend/{
|
|
10538
|
+
path: "/resend/{appOrSourceId}/emails/batch",
|
|
10387
10539
|
operationId: "resend.sendBatch",
|
|
10388
10540
|
tags: ["resend"],
|
|
10389
10541
|
summary: "Send up to 100 emails (Resend-compatible)",
|
|
10390
10542
|
security: API_KEY_AUTH,
|
|
10391
10543
|
surfaces: FACADE_SURFACES,
|
|
10392
10544
|
request: {
|
|
10393
|
-
params:
|
|
10545
|
+
params: targetParam,
|
|
10394
10546
|
body: jsonBody(resendBatchBodySchema)
|
|
10395
10547
|
},
|
|
10396
10548
|
responses: {
|
|
@@ -10403,13 +10555,13 @@ const resend = defineModule(defineRoute({
|
|
|
10403
10555
|
}
|
|
10404
10556
|
}), defineRoute({
|
|
10405
10557
|
method: "get",
|
|
10406
|
-
path: "/resend/{
|
|
10558
|
+
path: "/resend/{appOrSourceId}/emails/{id}",
|
|
10407
10559
|
operationId: "resend.get",
|
|
10408
10560
|
tags: ["resend"],
|
|
10409
10561
|
summary: "Fetch one sent email (Resend-compatible)",
|
|
10410
10562
|
security: API_KEY_AUTH,
|
|
10411
10563
|
surfaces: FACADE_SURFACES,
|
|
10412
|
-
request: { params:
|
|
10564
|
+
request: { params: targetParam.extend({ id: z.string().openapi({ example: "dlv_2f9a8c1b" }) }) },
|
|
10413
10565
|
responses: {
|
|
10414
10566
|
200: {
|
|
10415
10567
|
description: "The email object",
|
|
@@ -10896,8 +11048,18 @@ const mergedRedirect = { 307: {
|
|
|
10896
11048
|
headers: z.object({ Location: z.string().meta({ description: "The survivor's URL" }) })
|
|
10897
11049
|
} };
|
|
10898
11050
|
/**
|
|
10899
|
-
* Profiles by their Cowliss-generated id. `find`
|
|
10900
|
-
* the literal
|
|
11051
|
+
* Profiles by their Cowliss-generated id. `find`, `export` and `erase`
|
|
11052
|
+
* precede `{profileId}` so the literal paths win.
|
|
11053
|
+
*
|
|
11054
|
+
* Compliance has two ways in (ADR 0022). A `usr_` id names one profile, and
|
|
11055
|
+
* erasure or export takes it with everything merged into it. A
|
|
11056
|
+
* `{ kind, value }` identifier names a person, which is one profile per app
|
|
11057
|
+
* that knows the value, and the same erasure or export runs over all of
|
|
11058
|
+
* them at once: the addressing differs, nothing below it does. An identifier
|
|
11059
|
+
* never travels in the URL, neither in the path nor in a query string,
|
|
11060
|
+
* because a GDPR request carries an address and a URL is logged whole. That
|
|
11061
|
+
* is why both identifier routes are a POST carrying a body, including the
|
|
11062
|
+
* export, which reads.
|
|
10901
11063
|
*/
|
|
10902
11064
|
const users = defineModule(defineRoute({
|
|
10903
11065
|
method: "get",
|
|
@@ -10925,6 +11087,32 @@ const users = defineModule(defineRoute({
|
|
|
10925
11087
|
...sessionErrors,
|
|
10926
11088
|
...errors("not_found", "validation_failed")
|
|
10927
11089
|
}
|
|
11090
|
+
}), defineRoute({
|
|
11091
|
+
method: "post",
|
|
11092
|
+
path: "/v1/users/export",
|
|
11093
|
+
operationId: "users.exportByIdentifier",
|
|
11094
|
+
tags: ["users"],
|
|
11095
|
+
summary: "Export everything held on the people one identifier names",
|
|
11096
|
+
security: SESSION_AUTH,
|
|
11097
|
+
request: { body: jsonBody(exportUserByIdentifierBodySchema) },
|
|
11098
|
+
responses: {
|
|
11099
|
+
200: envelope(userExportSchema),
|
|
11100
|
+
...sessionErrors,
|
|
11101
|
+
...errors("not_found", "validation_failed", "malformed_request")
|
|
11102
|
+
}
|
|
11103
|
+
}), defineRoute({
|
|
11104
|
+
method: "post",
|
|
11105
|
+
path: "/v1/users/erase",
|
|
11106
|
+
operationId: "users.eraseByIdentifier",
|
|
11107
|
+
tags: ["users"],
|
|
11108
|
+
summary: "Erase every person one identifier names (GDPR)",
|
|
11109
|
+
security: SESSION_AUTH,
|
|
11110
|
+
request: { body: jsonBody(eraseUserByIdentifierBodySchema) },
|
|
11111
|
+
responses: {
|
|
11112
|
+
200: envelope(eraseUserResponseSchema),
|
|
11113
|
+
...sessionErrors,
|
|
11114
|
+
...errors("not_found", "validation_failed", "malformed_request")
|
|
11115
|
+
}
|
|
10928
11116
|
}), defineRoute({
|
|
10929
11117
|
method: "get",
|
|
10930
11118
|
path: "/v1/users/{profileId}",
|
|
@@ -11188,7 +11376,6 @@ const contract = {
|
|
|
11188
11376
|
violations,
|
|
11189
11377
|
review,
|
|
11190
11378
|
segments,
|
|
11191
|
-
project,
|
|
11192
11379
|
artifacts,
|
|
11193
11380
|
pushes,
|
|
11194
11381
|
versions,
|
|
@@ -11581,7 +11768,7 @@ function registerContractCommands(program, run) {
|
|
|
11581
11768
|
*/
|
|
11582
11769
|
/**
|
|
11583
11770
|
* One call, whichever way the keys were named: the route takes the set, and
|
|
11584
|
-
* `--all` is the
|
|
11771
|
+
* `--all` is the app id, resolved on the server against the rows the app
|
|
11585
11772
|
* owns (so it reaches a key whose file the tree lost). A key that is not
|
|
11586
11773
|
* there refuses the whole call, and nothing changes.
|
|
11587
11774
|
*/
|
|
@@ -11620,7 +11807,7 @@ const STATE_WORD = {
|
|
|
11620
11807
|
};
|
|
11621
11808
|
/** What the terminal says: one line per key, naming its new state. */
|
|
11622
11809
|
function flipSummary(journeys, status) {
|
|
11623
|
-
if (journeys.length === 0) return `This
|
|
11810
|
+
if (journeys.length === 0) return `This app has no journeys to put ${STATE_WORD[status]}.`;
|
|
11624
11811
|
return journeys.map((journey) => [`${journey.key} is ${STATE_WORD[status]}.`, ...segmentLines(journey)].join("\n")).join("\n");
|
|
11625
11812
|
}
|
|
11626
11813
|
const DESCRIPTIONS = {
|
|
@@ -11629,14 +11816,14 @@ const DESCRIPTIONS = {
|
|
|
11629
11816
|
paused: "hold journeys: nobody enters, and everyone part-way through waits"
|
|
11630
11817
|
};
|
|
11631
11818
|
function register(program, clientFor, io, verb, status) {
|
|
11632
|
-
program.command(verb).description(`${DESCRIPTIONS[status]} (many keys, or --all for this
|
|
11819
|
+
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
11820
|
const merged = {
|
|
11634
11821
|
...program.opts(),
|
|
11635
11822
|
...opts
|
|
11636
11823
|
};
|
|
11637
11824
|
if (merged.all === true && keys.length > 0) throw new Error(`Name the journeys or pass --all, not both: cow ${verb} welcome.`);
|
|
11638
11825
|
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 ? {
|
|
11826
|
+
const selector = merged.all === true ? { appId: (await assertCowConfig(process.cwd())).appId } : { keys };
|
|
11640
11827
|
const updated = await setStatus(await clientFor(merged), selector, status);
|
|
11641
11828
|
if (merged.json === true) {
|
|
11642
11829
|
emit({ data: updated }, io, true);
|
|
@@ -11696,43 +11883,45 @@ async function packageJson(name) {
|
|
|
11696
11883
|
}
|
|
11697
11884
|
});
|
|
11698
11885
|
}
|
|
11886
|
+
/** The org's active apps, or null when this machine cannot read them. */
|
|
11887
|
+
async function listApps(client) {
|
|
11888
|
+
try {
|
|
11889
|
+
return (await client.request(contract.apps["apps.list"], { query: {
|
|
11890
|
+
status: "active",
|
|
11891
|
+
limit: 100
|
|
11892
|
+
} })).data.map((app) => app.id);
|
|
11893
|
+
} catch {
|
|
11894
|
+
return null;
|
|
11895
|
+
}
|
|
11896
|
+
}
|
|
11699
11897
|
/**
|
|
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.
|
|
11898
|
+
* Pick the app this repository pushes to. One app is the answer; several
|
|
11899
|
+
* are a question; none is a refusal, because the answer is in the dashboard
|
|
11900
|
+
* and `cow init` is not allowed to invent one.
|
|
11706
11901
|
*/
|
|
11707
|
-
async function
|
|
11708
|
-
if (
|
|
11709
|
-
|
|
11710
|
-
|
|
11902
|
+
async function chooseApp(client, webUrl, flag, org, io, skipPrompts) {
|
|
11903
|
+
if (flag) return { appId: flag };
|
|
11904
|
+
if (org.claimed !== null && org.claimed !== org.requested) return {
|
|
11905
|
+
appId: "",
|
|
11906
|
+
reason: `this machine is logged in to ${org.claimed}, not ${org.requested}`
|
|
11711
11907
|
};
|
|
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
|
-
}
|
|
11908
|
+
const apps = await listApps(client);
|
|
11909
|
+
if (apps === null) return {
|
|
11910
|
+
appId: "",
|
|
11911
|
+
reason: "this machine could not read the org's apps"
|
|
11912
|
+
};
|
|
11913
|
+
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.`);
|
|
11914
|
+
const only = apps[0];
|
|
11915
|
+
if (apps.length === 1 && only) return { appId: only };
|
|
11916
|
+
if (skipPrompts || !io.isTTY) return {
|
|
11917
|
+
appId: "",
|
|
11918
|
+
reason: `this organization has several apps (${apps.join(", ")}) and nothing said which`
|
|
11919
|
+
};
|
|
11920
|
+
io.stderr(`Apps in this organization:\n${apps.map((id) => ` ${id}`).join("\n")}\n`);
|
|
11921
|
+
return { appId: await ask("App id", only ?? "", io, false) };
|
|
11733
11922
|
}
|
|
11734
|
-
function registerInit(program, env, io) {
|
|
11735
|
-
program.command("init").argument("[dir]", "directory to create the
|
|
11923
|
+
function registerInit(program, clientFor, env, io) {
|
|
11924
|
+
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
11925
|
const merged = {
|
|
11737
11926
|
...program.opts(),
|
|
11738
11927
|
...opts
|
|
@@ -11740,17 +11929,27 @@ function registerInit(program, env, io) {
|
|
|
11740
11929
|
const skipPrompts = opts.yes === true;
|
|
11741
11930
|
const cwd = process.cwd();
|
|
11742
11931
|
const defaultName = basename(dirArg ? resolve(cwd, dirArg) : cwd);
|
|
11743
|
-
const name = await ask("
|
|
11932
|
+
const name = await ask("Repo name", defaultName, io, skipPrompts);
|
|
11744
11933
|
const dir = dirArg ? resolve(cwd, dirArg) : name === defaultName ? cwd : resolve(cwd, name);
|
|
11745
11934
|
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
11935
|
const credentials = await readCredentials(env);
|
|
11747
11936
|
const claimedOrg = credentials ? decodeSessionToken(credentials.token)?.orgId ?? null : null;
|
|
11748
11937
|
const orgId = typeof opts.org === "string" && opts.org !== "" ? opts.org : await ask("Organization id", claimedOrg ?? "", io, skipPrompts);
|
|
11749
11938
|
if (orgId === "") throw new Error("No organization id. Pass --org org_… or run `cow login` first.");
|
|
11750
|
-
|
|
11939
|
+
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.`);
|
|
11940
|
+
const webUrl = resolveWebUrl(env);
|
|
11941
|
+
const app = await chooseApp(await clientFor(merged), webUrl, typeof opts.app === "string" && opts.app !== "" ? opts.app : void 0, {
|
|
11942
|
+
requested: orgId,
|
|
11943
|
+
claimed: claimedOrg
|
|
11944
|
+
}, io, skipPrompts);
|
|
11945
|
+
const config = app.appId === "" ? {
|
|
11751
11946
|
$schema: COW_CONFIG_SCHEMA_URL,
|
|
11752
11947
|
orgId,
|
|
11753
|
-
|
|
11948
|
+
appId: ""
|
|
11949
|
+
} : cowConfigSchema.parse({
|
|
11950
|
+
$schema: COW_CONFIG_SCHEMA_URL,
|
|
11951
|
+
orgId,
|
|
11952
|
+
appId: app.appId
|
|
11754
11953
|
});
|
|
11755
11954
|
const contents = {
|
|
11756
11955
|
"cow.json": json(config),
|
|
@@ -11773,19 +11972,18 @@ function registerInit(program, env, io) {
|
|
|
11773
11972
|
}
|
|
11774
11973
|
const example = typeof opts.example === "string" ? opts.example : void 0;
|
|
11775
11974
|
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
11975
|
emit({ data: {
|
|
11778
11976
|
dir,
|
|
11779
11977
|
orgId,
|
|
11780
11978
|
files,
|
|
11781
|
-
...
|
|
11979
|
+
...app
|
|
11782
11980
|
} }, io, merged.json);
|
|
11783
11981
|
const here = relative(cwd, dir) || ".";
|
|
11784
11982
|
const scenario = files.find((file) => file.startsWith("scenarios/")) ?? "scenarios/abandoned-checkout.timeout.json";
|
|
11785
|
-
const
|
|
11983
|
+
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
11984
|
io.stderr([
|
|
11787
|
-
`Created a cow
|
|
11788
|
-
|
|
11985
|
+
`Created a cow repo in ${here}`,
|
|
11986
|
+
appLine,
|
|
11789
11987
|
"",
|
|
11790
11988
|
"Next steps:",
|
|
11791
11989
|
` cd ${here}`,
|
|
@@ -11959,25 +12157,25 @@ function noGitHere(error) {
|
|
|
11959
12157
|
return failure.code === "ENOENT" || String(failure.stderr ?? "").includes("not a git repository");
|
|
11960
12158
|
}
|
|
11961
12159
|
/**
|
|
11962
|
-
* The push to restore: the one named, else this
|
|
11963
|
-
*
|
|
11964
|
-
*
|
|
12160
|
+
* The push to restore: the one named, else this app's newest. Scoped to this
|
|
12161
|
+
* directory's app, so a sibling repo's tree is never unpacked over this
|
|
12162
|
+
* one's files.
|
|
11965
12163
|
*
|
|
11966
12164
|
* The newest rather than the newest that compiled: a push stores the whole
|
|
11967
12165
|
* tree and its source archive whatever its keys then compiled to, so it is
|
|
11968
12166
|
* the last thing anyone pushed either way.
|
|
11969
12167
|
*/
|
|
11970
|
-
async function resolvePush(client, pushId,
|
|
12168
|
+
async function resolvePush(client, pushId, appId) {
|
|
11971
12169
|
if (pushId) return (await client.request(contract.pushes["pushes.get"], { params: { id: pushId } })).data;
|
|
11972
12170
|
const newest = (await client.request(contract.pushes["pushes.list"], { query: {
|
|
11973
12171
|
limit: 1,
|
|
11974
|
-
|
|
12172
|
+
appId
|
|
11975
12173
|
} })).data[0];
|
|
11976
|
-
if (!newest) throw new Error("This
|
|
12174
|
+
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
12175
|
return newest;
|
|
11978
12176
|
}
|
|
11979
12177
|
async function pullPush({ client, projectDir, pushId, force }) {
|
|
11980
|
-
const push = await resolvePush(client, pushId, await
|
|
12178
|
+
const push = await resolvePush(client, pushId, await configuredApp(projectDir));
|
|
11981
12179
|
if (!force) {
|
|
11982
12180
|
const dirty = await dirtyPaths(projectDir);
|
|
11983
12181
|
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 +12197,7 @@ async function pullPush({ client, projectDir, pushId, force }) {
|
|
|
11999
12197
|
};
|
|
12000
12198
|
}
|
|
12001
12199
|
function registerPull(program, clientFor, io) {
|
|
12002
|
-
program.command("pull").description("restore the
|
|
12200
|
+
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
12201
|
const merged = {
|
|
12004
12202
|
...program.opts(),
|
|
12005
12203
|
...opts
|
|
@@ -12020,25 +12218,6 @@ function registerPull(program, clientFor, io) {
|
|
|
12020
12218
|
|
|
12021
12219
|
//#endregion
|
|
12022
12220
|
//#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
12221
|
/** Does the org already hold these bytes? A 404 is the answer, not a failure. */
|
|
12043
12222
|
async function held(client, digest) {
|
|
12044
12223
|
try {
|
|
@@ -12072,8 +12251,6 @@ function artifactFiles(projectDir, manifest) {
|
|
|
12072
12251
|
}
|
|
12073
12252
|
async function pushProject({ client, projectDir }) {
|
|
12074
12253
|
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
12254
|
const uploaded = [];
|
|
12078
12255
|
for (const artifact of artifactFiles(projectDir, manifest)) {
|
|
12079
12256
|
if (await held(client, artifact.digest)) continue;
|
|
@@ -12090,7 +12267,8 @@ async function pushProject({ client, projectDir }) {
|
|
|
12090
12267
|
...manifest,
|
|
12091
12268
|
protocol: 3
|
|
12092
12269
|
},
|
|
12093
|
-
|
|
12270
|
+
appId: config.appId,
|
|
12271
|
+
orgId: config.orgId
|
|
12094
12272
|
} })).data,
|
|
12095
12273
|
uploaded
|
|
12096
12274
|
};
|
|
@@ -12106,10 +12284,10 @@ function pushSummary(outcome, webUrl) {
|
|
|
12106
12284
|
const stored = `Push #${push.seq} stored (${uploaded}).`;
|
|
12107
12285
|
if (push.versions.length === 0) return `${stored} Nothing changed.`;
|
|
12108
12286
|
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");
|
|
12287
|
+
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
12288
|
}
|
|
12111
12289
|
function registerPush(program, clientFor, env, io) {
|
|
12112
|
-
program.command("push").description("build
|
|
12290
|
+
program.command("push").description("build this repository and upload it; the server compiles what changed").action(async (opts) => {
|
|
12113
12291
|
const merged = {
|
|
12114
12292
|
...program.opts(),
|
|
12115
12293
|
...opts
|
|
@@ -12130,10 +12308,10 @@ function registerPush(program, clientFor, env, io) {
|
|
|
12130
12308
|
//#endregion
|
|
12131
12309
|
//#region src/commands/status.ts
|
|
12132
12310
|
/**
|
|
12133
|
-
* `cow status`: what the server holds for every key of this
|
|
12134
|
-
*
|
|
12135
|
-
*
|
|
12136
|
-
*
|
|
12311
|
+
* `cow status`: what the server holds for every key of this app, and how far
|
|
12312
|
+
* the tree in front of the developer has moved from it. Hand-written rather
|
|
12313
|
+
* than derived from `apps.status`, because the drift is the half of the
|
|
12314
|
+
* answer no server read can know: it comes from building the repository
|
|
12137
12315
|
* here and comparing the bundle digests with the versions the server has.
|
|
12138
12316
|
*/
|
|
12139
12317
|
/** Largest first: the first unit the gap fills is the one that reads best. */
|
|
@@ -12207,7 +12385,7 @@ function statusLines(status, manifest, now) {
|
|
|
12207
12385
|
const local = manifest ? localKeys(manifest) : [];
|
|
12208
12386
|
const journeys = status.keys.filter((entry) => entry.kind === "journey");
|
|
12209
12387
|
const templates = status.keys.filter((entry) => entry.kind === "template");
|
|
12210
|
-
const lines = [`${status.
|
|
12388
|
+
const lines = [`${status.appId}: ${journeys.length} ${journeys.length === 1 ? "journey" : "journeys"}, ${templates.length} ${templates.length === 1 ? "template" : "templates"} on the server.`];
|
|
12211
12389
|
const width = Math.max(0, ...status.keys.map((entry) => entry.key.length));
|
|
12212
12390
|
const section = (title, entries) => {
|
|
12213
12391
|
if (entries.length === 0) return;
|
|
@@ -12246,19 +12424,19 @@ function statusLines(status, manifest, now) {
|
|
|
12246
12424
|
}
|
|
12247
12425
|
return lines;
|
|
12248
12426
|
}
|
|
12249
|
-
/** The
|
|
12250
|
-
async function readStatus(client,
|
|
12251
|
-
return (await client.request(contract.
|
|
12427
|
+
/** The app's status from the server, for this directory's `cow.json`. */
|
|
12428
|
+
async function readStatus(client, appId) {
|
|
12429
|
+
return (await client.request(contract.apps["apps.status"], { params: { id: appId } })).data;
|
|
12252
12430
|
}
|
|
12253
12431
|
function registerStatus(program, clientFor, io) {
|
|
12254
|
-
program.command("status").description("what this
|
|
12432
|
+
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
12433
|
const merged = {
|
|
12256
12434
|
...program.opts(),
|
|
12257
12435
|
...opts
|
|
12258
12436
|
};
|
|
12259
12437
|
const config = await assertCowConfig(process.cwd());
|
|
12260
12438
|
const built = await buildProject(process.cwd()).catch(() => null);
|
|
12261
|
-
const status = await readStatus(await clientFor(merged), config.
|
|
12439
|
+
const status = await readStatus(await clientFor(merged), config.appId);
|
|
12262
12440
|
if (merged.json === true) {
|
|
12263
12441
|
emit({ data: status }, io, true);
|
|
12264
12442
|
return;
|
|
@@ -12527,7 +12705,7 @@ function emit(result, io, json) {
|
|
|
12527
12705
|
}
|
|
12528
12706
|
function buildProgram(env, io) {
|
|
12529
12707
|
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>", `
|
|
12708
|
+
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
12709
|
program.hook("preAction", () => {
|
|
12532
12710
|
const flag = program.opts().config;
|
|
12533
12711
|
setConfigFile((typeof flag === "string" ? flag : void 0) ?? env.COW_CONFIG ?? "cow.json");
|
|
@@ -12549,7 +12727,7 @@ function buildProgram(env, io) {
|
|
|
12549
12727
|
};
|
|
12550
12728
|
registerContractCommands(program, run);
|
|
12551
12729
|
registerAuth(program, env, io);
|
|
12552
|
-
registerInit(program, env, io);
|
|
12730
|
+
registerInit(program, clientFor, env, io);
|
|
12553
12731
|
registerAdd(program, io);
|
|
12554
12732
|
registerBuild(program, io);
|
|
12555
12733
|
registerPush(program, clientFor, env, io);
|