@cowliss/cli 0.11.0 → 0.12.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.
Files changed (2) hide show
  1. package/dist/index.js +232 -166
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -173,6 +173,8 @@ const SYSTEM_EVENT_PREFIX = "system.";
173
173
  * system.profile_deleted is the provider-recorded fact that a provider
174
174
  * (e.g. Clerk `user.deleted`) deleted the user upstream — a marker event
175
175
  * only, with no deletion semantics (hard-delete belongs to GDPR erasure);
176
+ * system.signed_up and system.logged_in are the same shape for the other end
177
+ * of that lifecycle (Clerk `user.created` and `session.created`);
176
178
  * system.email_clicked is a link click reported by SES feedback.
177
179
  *
178
180
  * Rows carrying one of these names get `system: true` so the usage meter
@@ -187,6 +189,17 @@ const SYSTEM_EVENTS = {
187
189
  consentRevoked: "system.consent_revoked",
188
190
  profileDeleted: "system.profile_deleted",
189
191
  /**
192
+ * The provider's account lifecycle, as facts rather than as profile state:
193
+ * a profile can predate its `user.created` (an SDK identify or a
194
+ * membership delivery may have created it first), so these name what the
195
+ * provider reported, not what Cowliss did. `signedUp` is the trigger for a
196
+ * welcome journey fed by a provider source, which `emailRegistered` only
197
+ * approximates (it fires on an address change too, and never for a
198
+ * profile with no address). `loggedIn` is one row per session start.
199
+ */
200
+ signedUp: "system.signed_up",
201
+ loggedIn: "system.logged_in",
202
+ /**
190
203
  * A write's identifiers named more than one profile and Cowliss merged
191
204
  * them (ticket 69). Lands on the survivor's timeline carrying
192
205
  * `mergedProfileIds`, the ids folded into it.
@@ -7873,6 +7886,36 @@ const updateWebhookBodySchema = z.object({ data: z.object({
7873
7886
  /** `q` is a substring search over the webhook's name. */
7874
7887
  const listWebhooksQuerySchema = paginationQuerySchema.extend({ q: searchQuerySchema });
7875
7888
 
7889
+ //#endregion
7890
+ //#region ../../packages/shared/src/whoami.ts
7891
+ /**
7892
+ * What an ingestion key plus one source id write into. An org API key is
7893
+ * org-scoped and carries no app of its own (`ApiKeyClaims`), so any live
7894
+ * `src_` in the organization is accepted by the write path: a well-formed id
7895
+ * belonging to another app lands profiles there with a 200. The id the
7896
+ * caller is about to send is therefore the input, and this answers where it
7897
+ * goes before anything is written.
7898
+ *
7899
+ * Scoped to that one source's app on purpose: the key stays a write
7900
+ * credential, and a leaked one must not enumerate the org's other apps
7901
+ * (.scratch/24-onboarding-dx/spec.md).
7902
+ */
7903
+ const whoamiQuerySchema = z.object({ sourceId: sourceIdSchema });
7904
+ const whoamiSchema = z.object({
7905
+ orgId: z.string(),
7906
+ app: z.object({
7907
+ id: z.string(),
7908
+ name: z.string()
7909
+ }),
7910
+ /** Every live pipe into that app, so "a Clerk source already feeds this" is visible. */
7911
+ sources: z.array(sourceSchema.pick({
7912
+ id: true,
7913
+ kind: true,
7914
+ configured: true,
7915
+ lastReceivedAt: true
7916
+ }))
7917
+ });
7918
+
7876
7919
  //#endregion
7877
7920
  //#region ../../packages/api-client/src/client.ts
7878
7921
  var ApiError = class extends Error {
@@ -7897,7 +7940,7 @@ async function send(baseUrl, init) {
7897
7940
  body: typeof init.body === "string" || init.body === null ? init.body : Uint8Array.from(init.body)
7898
7941
  });
7899
7942
  } catch {
7900
- throw new ApiError("network_error", `Could not reach the API at ${baseUrl}. Is apps/api running?`, 0);
7943
+ throw new ApiError("network_error", `Could not reach the API at ${baseUrl}. Check the address, or set COW_API_URL to the one you meant.`, 0);
7901
7944
  }
7902
7945
  const requestId = response.headers.get("X-Request-Id") ?? void 0;
7903
7946
  if (init.binary && response.ok) return {
@@ -9211,170 +9254,6 @@ function registerAdd(program, io) {
9211
9254
  });
9212
9255
  }
9213
9256
 
9214
- //#endregion
9215
- //#region src/lib/login.ts
9216
- /**
9217
- * Login via the dashboard's /cli-auth bridge: start a loopback server, open
9218
- * the web app there, and the signed-in browser POSTs the Clerk session token
9219
- * back. `--token <jwt>` bypasses the browser entirely (agents, CI).
9220
- */
9221
- async function login(env, options) {
9222
- let token;
9223
- if (options.token !== void 0) {
9224
- token = options.token.trim();
9225
- if (decodeTokenPayload(token) === null) throw new Error("The provided --token value is not a JWT");
9226
- } else token = await collectTokenViaBrowser(resolveWebUrl(env, (await readCowConfig(process.cwd()))?.webUrl, options.webUrl), 3e5, options.noOpen === true);
9227
- const claims = decodeSessionToken(token);
9228
- if (claims === null) throw new Error("Token is not a decodable JWT");
9229
- if (claims.userId === null) throw new Error("Token has no subject claim. Is it a Clerk session token?");
9230
- const apiUrl = options.apiFlag ?? resolveApiUrl(env, null, void 0, (await readCowConfig(process.cwd()))?.apiUrl);
9231
- return {
9232
- credentialsPath: await writeCredentials(env, {
9233
- token,
9234
- apiUrl
9235
- }),
9236
- apiUrl,
9237
- userId: claims.userId,
9238
- orgId: claims.orgId,
9239
- role: claims.role,
9240
- hasOrg: claims.orgId !== null
9241
- };
9242
- }
9243
- /** Open the dashboard bridge and await one POST of { token }. */
9244
- function collectTokenViaBrowser(webUrl, timeoutMs = 18e4, noOpen = false) {
9245
- return new Promise((resolve, reject) => {
9246
- let server = null;
9247
- const timer = setTimeout(() => {
9248
- server?.close();
9249
- reject(/* @__PURE__ */ new Error(`Timed out waiting for the browser to sign in (${Math.round(timeoutMs / 1e3)}s). Or paste a token: \`cow login --token <jwt>\`.`));
9250
- }, timeoutMs);
9251
- server = createServer((req, res) => {
9252
- res.setHeader("Access-Control-Allow-Origin", "*");
9253
- res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
9254
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
9255
- if (req.method === "OPTIONS") {
9256
- res.writeHead(204).end();
9257
- return;
9258
- }
9259
- if (req.method !== "POST") {
9260
- res.writeHead(405).end();
9261
- return;
9262
- }
9263
- const chunks = [];
9264
- req.on("data", (chunk) => chunks.push(chunk));
9265
- req.on("end", () => {
9266
- clearTimeout(timer);
9267
- try {
9268
- const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
9269
- const field = (name) => typeof body === "object" && body !== null && name in body ? body[name] : void 0;
9270
- if (field("denied") === true) {
9271
- res.writeHead(200, { "Content-Type": "application/json" });
9272
- res.end(JSON.stringify({ ok: true }));
9273
- reject(/* @__PURE__ */ new Error("Sign-in was denied in the browser. No access was granted."));
9274
- return;
9275
- }
9276
- const token = field("token");
9277
- if (typeof token !== "string" || token.length === 0) {
9278
- res.writeHead(400, { "Content-Type": "application/json" });
9279
- res.end(JSON.stringify({ error: "missing token" }));
9280
- reject(/* @__PURE__ */ new Error("The browser bridge sent no token"));
9281
- return;
9282
- }
9283
- res.writeHead(200, { "Content-Type": "application/json" });
9284
- res.end(JSON.stringify({ ok: true }));
9285
- resolve(token);
9286
- } catch (error) {
9287
- reject(error instanceof Error ? error : new Error(String(error)));
9288
- } finally {
9289
- server?.close();
9290
- }
9291
- });
9292
- });
9293
- server.listen(0, "127.0.0.1", () => {
9294
- const address = server?.address();
9295
- if (address === null || typeof address !== "object") {
9296
- reject(/* @__PURE__ */ new Error("Could not bind a loopback port"));
9297
- return;
9298
- }
9299
- const callbackUrl = `http://127.0.0.1:${address.port}/cb`;
9300
- const target = `${webUrl}/cli-auth?cb=${encodeURIComponent(callbackUrl)}`;
9301
- if (!noOpen) openBrowser(target);
9302
- process.stderr.write(`Waiting for sign-in at:\n ${target}\n`);
9303
- });
9304
- server.on("error", (error) => {
9305
- clearTimeout(timer);
9306
- reject(error);
9307
- });
9308
- });
9309
- }
9310
- function openBrowser(url) {
9311
- const cmd = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : null;
9312
- if (cmd === null) return;
9313
- spawn(cmd, [url], {
9314
- stdio: "ignore",
9315
- detached: true
9316
- }).unref();
9317
- }
9318
-
9319
- //#endregion
9320
- //#region src/commands/auth.ts
9321
- /** login/logout/whoami: session-token management, no contract route. */
9322
- function registerAuth(program, env, io) {
9323
- 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) => {
9324
- const outcome = await login(env, {
9325
- token: opts.token,
9326
- webUrl: opts.web,
9327
- apiFlag: opts.api,
9328
- noOpen: opts.open === false
9329
- });
9330
- emit({ data: {
9331
- ...outcome,
9332
- warnings: outcome.hasOrg ? void 0 : ["The token carries no active organization. Select one in the dashboard and log in again."]
9333
- } }, io, opts.json);
9334
- });
9335
- program.command("logout").description("delete the cached session token").action(async (opts) => {
9336
- const removed = await clearCredentials(env);
9337
- emit({ data: { removed } }, io, opts.json);
9338
- });
9339
- program.command("whoami").description("show which credential commands will use, and for a session its subject, org, role, and expiry").action(async (opts) => {
9340
- const credentials = await readCredentials(env);
9341
- const { kind } = resolveCredential(env, credentials);
9342
- if (kind === "none") throw new Error("Not logged in. Run `cow login` first, or set COW_PIPELINE_KEY.");
9343
- if (kind === "pipelineKey") {
9344
- emit({ data: {
9345
- credential: kind,
9346
- source: "COW_PIPELINE_KEY",
9347
- note: "A pipeline key may push, enable, disable, and read executions."
9348
- } }, io, opts.json);
9349
- return;
9350
- }
9351
- const claims = decodeSessionToken(credentials?.token ?? "");
9352
- if (!claims) throw new Error("Cached token is not decodable. Run `cow login` again.");
9353
- const projectApiUrl = (await readCowConfig(process.cwd()))?.apiUrl;
9354
- const apiUrl = resolveApiUrl(env, credentials, opts.api, projectApiUrl);
9355
- const mintedFor = credentials?.apiUrl;
9356
- emit({ data: {
9357
- credential: kind,
9358
- apiUrl,
9359
- mintedFor: mintedFor ?? null,
9360
- ...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.` } : {},
9361
- ...claims
9362
- } }, io, opts.json);
9363
- });
9364
- }
9365
-
9366
- //#endregion
9367
- //#region src/commands/build.ts
9368
- /**
9369
- * `cow build`: typecheck, bundle, and write `.cow/build` for the project in
9370
- * the current directory. Local only, no API call and no network.
9371
- */
9372
- function registerBuild(program, io) {
9373
- program.command("build").description("typecheck and bundle the repo in the current directory, writing .cow/build (local: no API call)").action(async (opts) => {
9374
- emit({ data: await buildProject(process.cwd()) }, io, opts.json);
9375
- });
9376
- }
9377
-
9378
9257
  //#endregion
9379
9258
  //#region ../../node_modules/.pnpm/@asteasolutions+zod-to-openapi@9.1.0_zod@4.4.3/node_modules/@asteasolutions/zod-to-openapi/dist/index.cjs
9380
9259
  var require_dist = /* @__PURE__ */ __commonJSMin(((exports) => {
@@ -10659,6 +10538,19 @@ const ingestion = defineModule(defineRoute({
10659
10538
  200: envelope(batchResultDtoSchema, "Per-item outcomes, positionally aligned"),
10660
10539
  ...ingestionErrors
10661
10540
  }
10541
+ }), defineRoute({
10542
+ method: "get",
10543
+ path: "/v1/whoami",
10544
+ operationId: "ingestion.whoami",
10545
+ tags: ["ingestion"],
10546
+ summary: "Which app a source id writes into",
10547
+ security: API_KEY_AUTH,
10548
+ surfaces: HIDDEN_FROM_TOOLS,
10549
+ request: { query: whoamiQuerySchema },
10550
+ responses: {
10551
+ 200: envelope(whoamiSchema, "The source's org, app, and sibling sources"),
10552
+ ...errors("invalid_key", "not_found", "validation_failed", "rate_limited")
10553
+ }
10662
10554
  }));
10663
10555
 
10664
10556
  //#endregion
@@ -11887,6 +11779,180 @@ const allRoutes = Object.values(contract).flatMap((module) => Object.values(modu
11887
11779
  /** The tags in the order their modules are declared. */
11888
11780
  const tags = [...new Set(allRoutes.flatMap((route) => route.tags ?? []))];
11889
11781
 
11782
+ //#endregion
11783
+ //#region src/lib/login.ts
11784
+ /**
11785
+ * Login via the dashboard's /cli-auth bridge: start a loopback server, open
11786
+ * the web app there, and the signed-in browser POSTs the Clerk session token
11787
+ * back. `--token <jwt>` bypasses the browser entirely (agents, CI).
11788
+ */
11789
+ async function login(env, options) {
11790
+ let token;
11791
+ if (options.token !== void 0) {
11792
+ token = options.token.trim();
11793
+ if (decodeTokenPayload(token) === null) throw new Error("The provided --token value is not a JWT");
11794
+ } else token = await collectTokenViaBrowser(resolveWebUrl(env, (await readCowConfig(process.cwd()))?.webUrl, options.webUrl), 3e5, options.noOpen === true);
11795
+ const claims = decodeSessionToken(token);
11796
+ if (claims === null) throw new Error("Token is not a decodable JWT");
11797
+ if (claims.userId === null) throw new Error("Token has no subject claim. Is it a Clerk session token?");
11798
+ const apiUrl = options.apiFlag ?? resolveApiUrl(env, null, void 0, (await readCowConfig(process.cwd()))?.apiUrl);
11799
+ return {
11800
+ credentialsPath: await writeCredentials(env, {
11801
+ token,
11802
+ apiUrl
11803
+ }),
11804
+ apiUrl,
11805
+ userId: claims.userId,
11806
+ orgId: claims.orgId,
11807
+ role: claims.role,
11808
+ hasOrg: claims.orgId !== null
11809
+ };
11810
+ }
11811
+ /** Open the dashboard bridge and await one POST of { token }. */
11812
+ function collectTokenViaBrowser(webUrl, timeoutMs = 18e4, noOpen = false) {
11813
+ return new Promise((resolve, reject) => {
11814
+ let server = null;
11815
+ const timer = setTimeout(() => {
11816
+ server?.close();
11817
+ reject(/* @__PURE__ */ new Error(`Timed out waiting for the browser to sign in (${Math.round(timeoutMs / 1e3)}s). Or paste a token: \`cow login --token <jwt>\`.`));
11818
+ }, timeoutMs);
11819
+ server = createServer((req, res) => {
11820
+ res.setHeader("Access-Control-Allow-Origin", "*");
11821
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
11822
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
11823
+ if (req.method === "OPTIONS") {
11824
+ res.writeHead(204).end();
11825
+ return;
11826
+ }
11827
+ if (req.method !== "POST") {
11828
+ res.writeHead(405).end();
11829
+ return;
11830
+ }
11831
+ const chunks = [];
11832
+ req.on("data", (chunk) => chunks.push(chunk));
11833
+ req.on("end", () => {
11834
+ clearTimeout(timer);
11835
+ try {
11836
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
11837
+ const field = (name) => typeof body === "object" && body !== null && name in body ? body[name] : void 0;
11838
+ if (field("denied") === true) {
11839
+ res.writeHead(200, { "Content-Type": "application/json" });
11840
+ res.end(JSON.stringify({ ok: true }));
11841
+ reject(/* @__PURE__ */ new Error("Sign-in was denied in the browser. No access was granted."));
11842
+ return;
11843
+ }
11844
+ const token = field("token");
11845
+ if (typeof token !== "string" || token.length === 0) {
11846
+ res.writeHead(400, { "Content-Type": "application/json" });
11847
+ res.end(JSON.stringify({ error: "missing token" }));
11848
+ reject(/* @__PURE__ */ new Error("The browser bridge sent no token"));
11849
+ return;
11850
+ }
11851
+ res.writeHead(200, { "Content-Type": "application/json" });
11852
+ res.end(JSON.stringify({ ok: true }));
11853
+ resolve(token);
11854
+ } catch (error) {
11855
+ reject(error instanceof Error ? error : new Error(String(error)));
11856
+ } finally {
11857
+ server?.close();
11858
+ }
11859
+ });
11860
+ });
11861
+ server.listen(0, "127.0.0.1", () => {
11862
+ const address = server?.address();
11863
+ if (address === null || typeof address !== "object") {
11864
+ reject(/* @__PURE__ */ new Error("Could not bind a loopback port"));
11865
+ return;
11866
+ }
11867
+ const callbackUrl = `http://127.0.0.1:${address.port}/cb`;
11868
+ const target = `${webUrl}/cli-auth?cb=${encodeURIComponent(callbackUrl)}`;
11869
+ if (!noOpen) openBrowser(target);
11870
+ process.stderr.write(`Waiting for sign-in at:\n ${target}\n`);
11871
+ });
11872
+ server.on("error", (error) => {
11873
+ clearTimeout(timer);
11874
+ reject(error);
11875
+ });
11876
+ });
11877
+ }
11878
+ function openBrowser(url) {
11879
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : null;
11880
+ if (cmd === null) return;
11881
+ spawn(cmd, [url], {
11882
+ stdio: "ignore",
11883
+ detached: true
11884
+ }).unref();
11885
+ }
11886
+
11887
+ //#endregion
11888
+ //#region src/commands/auth.ts
11889
+ /** login/logout/whoami: session-token management, no contract route. */
11890
+ function registerAuth(program, env, io) {
11891
+ 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) => {
11892
+ const outcome = await login(env, {
11893
+ token: opts.token,
11894
+ webUrl: opts.web,
11895
+ apiFlag: opts.api,
11896
+ noOpen: opts.open === false
11897
+ });
11898
+ emit({ data: {
11899
+ ...outcome,
11900
+ warnings: outcome.hasOrg ? void 0 : ["The token carries no active organization. Select one in the dashboard and log in again."]
11901
+ } }, io, opts.json);
11902
+ });
11903
+ program.command("logout").description("delete the cached session token").action(async (opts) => {
11904
+ const removed = await clearCredentials(env);
11905
+ emit({ data: { removed } }, io, opts.json);
11906
+ });
11907
+ program.command("whoami").description("show which credential commands will use, and for a session its subject, org, role, and expiry").option("--key <ak_...>", "ask the API what an ingestion key writes into, instead of reading the cached session").option("--source <src_...>", "the source id that key is about to send to (required with --key)").action(async (opts) => {
11908
+ if (opts.key) {
11909
+ if (!opts.source) throw new Error("`--key` needs `--source <src_...>`: the id you are about to send to is what this checks.");
11910
+ const client = createClient({
11911
+ baseUrl: resolveApiUrl(env, await readCredentials(env), opts.api, (await readCowConfig(process.cwd()))?.apiUrl),
11912
+ auth: () => opts.key ?? null,
11913
+ headers: { [CLIENT_HEADER]: `@cowliss/cli/${(await readCliPackage()).version}` }
11914
+ });
11915
+ emit(await client.request(contract.ingestion["ingestion.whoami"], { query: { sourceId: opts.source } }), io, opts.json);
11916
+ return;
11917
+ }
11918
+ const credentials = await readCredentials(env);
11919
+ const { kind } = resolveCredential(env, credentials);
11920
+ if (kind === "none") throw new Error("Not logged in. Run `cow login` first, or set COW_PIPELINE_KEY.");
11921
+ if (kind === "pipelineKey") {
11922
+ emit({ data: {
11923
+ credential: kind,
11924
+ source: "COW_PIPELINE_KEY",
11925
+ note: "A pipeline key may push, enable, disable, and read executions."
11926
+ } }, io, opts.json);
11927
+ return;
11928
+ }
11929
+ const claims = decodeSessionToken(credentials?.token ?? "");
11930
+ if (!claims) throw new Error("Cached token is not decodable. Run `cow login` again.");
11931
+ const projectApiUrl = (await readCowConfig(process.cwd()))?.apiUrl;
11932
+ const apiUrl = resolveApiUrl(env, credentials, opts.api, projectApiUrl);
11933
+ const mintedFor = credentials?.apiUrl;
11934
+ emit({ data: {
11935
+ credential: kind,
11936
+ apiUrl,
11937
+ mintedFor: mintedFor ?? null,
11938
+ ...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.` } : {},
11939
+ ...claims
11940
+ } }, io, opts.json);
11941
+ });
11942
+ }
11943
+
11944
+ //#endregion
11945
+ //#region src/commands/build.ts
11946
+ /**
11947
+ * `cow build`: typecheck, bundle, and write `.cow/build` for the project in
11948
+ * the current directory. Local only, no API call and no network.
11949
+ */
11950
+ function registerBuild(program, io) {
11951
+ program.command("build").description("typecheck and bundle the repo in the current directory, writing .cow/build (local: no API call)").action(async (opts) => {
11952
+ emit({ data: await buildProject(process.cwd()) }, io, opts.json);
11953
+ });
11954
+ }
11955
+
11890
11956
  //#endregion
11891
11957
  //#region ../../packages/shared/src/contract/surfaces.ts
11892
11958
  /** camelCase field name → kebab-case flag body (`sourceId` → `source-id`). */
@@ -13591,7 +13657,7 @@ function errorMessage(error) {
13591
13657
  code: error.code,
13592
13658
  message: error.message
13593
13659
  } }, null, 2);
13594
- if (error.code === "invalid_key") return `${envelope}\n(hint: run \`cow login\` to refresh the session token)`;
13660
+ if (error.code === "invalid_key" && !process.argv.includes("--key")) return `${envelope}\n(hint: run \`cow login\` to refresh the session token)`;
13595
13661
  return envelope;
13596
13662
  }
13597
13663
  if (error instanceof Error) return `error: ${error.message}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cowliss/cli",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "cow": "./dist/index.js"