@stardeck-customer-apps/testing 0.15.0 → 0.17.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/SKILL.md CHANGED
@@ -341,6 +341,64 @@ externalId })` helper models a trusted platform/channel link, and
341
341
  `.merge(sourceId, canonicalId)` models governed merge redirects. They are
342
342
  setup helpers only; deployed app code cannot create verified links or merge
343
343
  identities through the integrations SDK.
344
+ - **Profile connections** (`client.identities.claims`) are driven from the same
345
+ accessor: `.claims()` and `.claim(claimId)` inspect what the app opened;
346
+ `.enableStaffAssist()` turns on the org opt-in that `assist`/`transfer` claims
347
+ need (off by default, so an un-opted project gets the real 403);
348
+ `.completeClaim(claimId, { clerkUserId })` models the customer finishing the
349
+ platform-hosted flow; `.refuseClaim(claimId, reason)` models reception or the
350
+ platform turning one down. `completeClaim` flips the claimed contact to
351
+ `verified`, or records `invalidated` when the contact is no longer on that
352
+ profile and `refused` / `contact_owned` when someone else already proved it.
353
+ Completing a finished claim replays the recorded result. Completion attaches
354
+ the claimant's login to the profile (moving it off any profile it was on), but
355
+ does not move history — the platform's merge core is not modelled.
356
+ `.allowReturnOrigin(origin)` widens the allowed `returnTo` origins (the test
357
+ app's own origin is allowed by default); a `returnTo` anywhere else is dropped
358
+ silently, exactly as the platform drops it. `claimUrl` carries a one-time
359
+ opaque handle, so never assert it contains the claim id, and `deliver: true`
360
+ comes back as `delivered`. `.suppressClaimDelivery()` models the platform
361
+ sending nothing (a suppressed quota, a bounced address): `deliver: true` then
362
+ returns `delivered: false` with the `claimUrl` still valid, so the app's own
363
+ fallback delivery can be tested. Bearer links are on by default, like the platform;
364
+ `.enableBearerLinks(false)` models a project whose admin turned them off. A bearer claim is raised against a
365
+ profile with no contact at all, so it takes no `kind`/`externalId`/`deliver`
366
+ (sending one is refused, not ignored) and `completeClaim` connects whoever
367
+ completes it — unless another login already holds that profile, which comes
368
+ back `refused` / `owned_elsewhere`. That refusal is the one a leaked bearer
369
+ link runs into, so test it. A profile that holds an email or phone is refused
370
+ at `create` with a 409 (send an `invite`); one that gains a contact before
371
+ the link is used completes as `refused` / `protected`, and a link still
372
+ outstanding when `.enableBearerLinks(false)` runs completes as `refused` /
373
+ `expired`. People seeded with only a `dashboard_user` link (team members) are
374
+ left out of `identities.list()` and `search()`, as on the platform.
375
+ `search({ attributes })` matches profile attributes exactly and by JSON type
376
+ (`5` never matches `"5"`) and rejects nested, array, `null` or more than 10
377
+ keys (or a `__proto__` key) with a `ValidationError` whose message matches the
378
+ platform's, like the platform.
379
+
380
+ ```ts
381
+ const claim = await integrations.identities.claims.create({
382
+ flow: "invite",
383
+ identityId: person.id,
384
+ kind: "phone",
385
+ externalId: "+15550100",
386
+ });
387
+ app.identities.completeClaim(claim.claimId, { clerkUserId: "user_1" });
388
+ expect((await integrations.identities.claims.get(claim.claimId)).state).toBe("completed");
389
+ ```
390
+
391
+ ```ts
392
+ // A name on a card, no email or phone: the link IS the authorisation.
393
+ const bearer = await integrations.identities.claims.create({
394
+ flow: "bearer",
395
+ identityId: walkIn.id,
396
+ });
397
+ expect(app.identities.completeClaim(bearer.claimId, { clerkUserId: "user_2" }).state).toBe(
398
+ "completed"
399
+ );
400
+ ```
401
+
344
402
  - `app.payments` — checkouts created through payments-sdk:
345
403
  `.checkouts`, `.latest()`, `.setProducts()`, `.markPaid(id)`,
346
404
  `.setSessionStatus(id, status)`, `.setPaymentLinkStatus(id, status)`,
@@ -409,12 +467,20 @@ describe the code, and the owner can't tell from them what is or isn't covered.
409
467
  A–Z→a–z; Turkish İ is left unchanged), requires E.164 phone values, and
410
468
  always writes an unverified link. A guest lookup of a verified link returns
411
469
  `{ identityId: null, reason: "verified_conflict" }`; staff provenance may
412
- resolve either verified or unverified links. `attachLink` accepts only
413
- channel kinds (line/facebook/instagram/email/phone); login keys must be
414
- seeded via `app.identities.seedVerifiedLink`. `attachLink(..., { verified:
470
+ resolve either verified or unverified links. `attachLink` records a contact or
471
+ channel id (line/facebook/instagram/email/phone); login keys must be seeded
472
+ via `app.identities.seedVerifiedLink`. `attachLink(..., { verified:
415
473
  true })` remains source-compatible but the simulator (like the control plane)
416
- ignores that flag and returns `verified: false`. `update` replaces the
417
- `profile` object (not a merge), like the control plane. Inspect via
474
+ ignores that flag and returns `verified: false`. An **unverified** email or
475
+ phone may be recorded on several identities (a household sharing one number);
476
+ a verified holder makes a further _verified_ attach conflict, but unverified
477
+ copies still succeed, and `resolveOrCreate` prefers the verified owner and
478
+ otherwise the earliest unverified holder. Channel and login kinds stay
479
+ single-owner and 409 on a second holder. `update` replaces the
480
+ `profile` object (not a merge), like the control plane. A person with a
481
+ seeded `dashboard_user` link is a team member: changing their `displayName`
482
+ or `profile.email` is refused with a 409 `ValidationError`, and a new
483
+ `profile` without `email` keeps the current one, as on the platform. Inspect via
418
484
  `app.identities`; merge/archive are privileged setup operations, with only
419
485
  test-only merge modeling exposed above.
420
486
  - `session.user.identityId` is the platform-provisioned cross-app customer key.
package/dist/index.d.mts CHANGED
@@ -105,6 +105,33 @@ interface CapturedIdentityLink {
105
105
  * the control plane that `client.identities` talks to in tests. Identities and
106
106
  * links created by app code under test land here; assert on them like `inbox`.
107
107
  */
108
+ interface CapturedIdentityClaim {
109
+ claimId: string;
110
+ flow: "invite" | "assist" | "transfer" | "bearer";
111
+ /** The profile the claim connects the customer to. */
112
+ identityId: string;
113
+ /** The recorded contact this claim was raised against. Null for `bearer` only. */
114
+ kind: "email" | "phone" | null;
115
+ externalId: string | null;
116
+ returnTo: string | null;
117
+ /** The app's reference for the staff member, for assist/transfer claims. */
118
+ staffActorRef: string | null;
119
+ state: "pending" | "completed" | "expired" | "invalidated" | "refused";
120
+ refusalReason: string | null;
121
+ completedIdentityId: string | null;
122
+ /** Set by completeClaim; models the Clerk account that proved the contact. */
123
+ claimantClerkUserId: string | null;
124
+ /**
125
+ * Hosted claim link. Its path carries a one-time opaque handle, NOT the claim
126
+ * id — the platform never makes the customer's page derivable from an id an
127
+ * app already holds.
128
+ */
129
+ claimUrl: string;
130
+ /** Whether the platform sent the link itself (only when `deliver` was asked). */
131
+ delivered: boolean | null;
132
+ expiresAt: string;
133
+ createdAt: string;
134
+ }
108
135
  interface TestDirectory {
109
136
  /** All identities, oldest first. */
110
137
  all(): CapturedIdentity[];
@@ -126,6 +153,49 @@ interface TestDirectory {
126
153
  * identity. The production integrations SDK does not expose merge operations.
127
154
  */
128
155
  merge(sourceIdentityId: string, canonicalIdentityId: string): void;
156
+ /** Every profile-connection claim the app has opened, oldest first. */
157
+ claims(): CapturedIdentityClaim[];
158
+ /** A single claim by id, or undefined. */
159
+ claim(claimId: string): CapturedIdentityClaim | undefined;
160
+ /**
161
+ * Test setup only: turn on the org's staff-assistance opt-in. Without it,
162
+ * `claims.create` refuses `assist` and `transfer` with 403, like the platform.
163
+ */
164
+ enableStaffAssist(enabled?: boolean): void;
165
+ /**
166
+ * Test setup only: set the project's bearer-link opt-in. It is on by default,
167
+ * like the platform; pass `false` to model an org that switched it off, and
168
+ * `claims.create({ flow: "bearer" })` is then refused with 403.
169
+ */
170
+ enableBearerLinks(enabled?: boolean): void;
171
+ /**
172
+ * Test setup only: model the platform failing to send a claim link it was
173
+ * asked to deliver (a suppressed quota, a bounced address). `deliver: true`
174
+ * then comes back `delivered: false` with the `claimUrl` still valid, so an
175
+ * app's own fallback delivery can be tested.
176
+ */
177
+ suppressClaimDelivery(enabled?: boolean): void;
178
+ /**
179
+ * Test setup only: allow a claim `returnTo` on this origin. The harness app's
180
+ * own origin is allowed by default; anything else is dropped silently, like
181
+ * the platform's allowed-origins check.
182
+ */
183
+ allowReturnOrigin(origin: string): void;
184
+ /**
185
+ * Test setup only: model the customer finishing the platform-hosted claim —
186
+ * Clerk proof plus their confirmation. Flips the claimed contact to verified,
187
+ * or records `invalidated` (the contact was removed from that profile) or
188
+ * `refused` with `contact_owned` (someone else already proved it). Replaying a
189
+ * finished claim returns the recorded result unchanged.
190
+ *
191
+ * It does NOT attach a login link or move history: the platform's merge core
192
+ * is not modelled here, only the ownership decision the app can observe.
193
+ */
194
+ completeClaim(claimId: string, params: {
195
+ clerkUserId: string;
196
+ }): CapturedIdentityClaim;
197
+ /** Test setup only: model reception or the platform refusing a claim. */
198
+ refuseClaim(claimId: string, reason: string): CapturedIdentityClaim;
129
199
  clear(): void;
130
200
  get count(): number;
131
201
  }
package/dist/index.d.ts CHANGED
@@ -105,6 +105,33 @@ interface CapturedIdentityLink {
105
105
  * the control plane that `client.identities` talks to in tests. Identities and
106
106
  * links created by app code under test land here; assert on them like `inbox`.
107
107
  */
108
+ interface CapturedIdentityClaim {
109
+ claimId: string;
110
+ flow: "invite" | "assist" | "transfer" | "bearer";
111
+ /** The profile the claim connects the customer to. */
112
+ identityId: string;
113
+ /** The recorded contact this claim was raised against. Null for `bearer` only. */
114
+ kind: "email" | "phone" | null;
115
+ externalId: string | null;
116
+ returnTo: string | null;
117
+ /** The app's reference for the staff member, for assist/transfer claims. */
118
+ staffActorRef: string | null;
119
+ state: "pending" | "completed" | "expired" | "invalidated" | "refused";
120
+ refusalReason: string | null;
121
+ completedIdentityId: string | null;
122
+ /** Set by completeClaim; models the Clerk account that proved the contact. */
123
+ claimantClerkUserId: string | null;
124
+ /**
125
+ * Hosted claim link. Its path carries a one-time opaque handle, NOT the claim
126
+ * id — the platform never makes the customer's page derivable from an id an
127
+ * app already holds.
128
+ */
129
+ claimUrl: string;
130
+ /** Whether the platform sent the link itself (only when `deliver` was asked). */
131
+ delivered: boolean | null;
132
+ expiresAt: string;
133
+ createdAt: string;
134
+ }
108
135
  interface TestDirectory {
109
136
  /** All identities, oldest first. */
110
137
  all(): CapturedIdentity[];
@@ -126,6 +153,49 @@ interface TestDirectory {
126
153
  * identity. The production integrations SDK does not expose merge operations.
127
154
  */
128
155
  merge(sourceIdentityId: string, canonicalIdentityId: string): void;
156
+ /** Every profile-connection claim the app has opened, oldest first. */
157
+ claims(): CapturedIdentityClaim[];
158
+ /** A single claim by id, or undefined. */
159
+ claim(claimId: string): CapturedIdentityClaim | undefined;
160
+ /**
161
+ * Test setup only: turn on the org's staff-assistance opt-in. Without it,
162
+ * `claims.create` refuses `assist` and `transfer` with 403, like the platform.
163
+ */
164
+ enableStaffAssist(enabled?: boolean): void;
165
+ /**
166
+ * Test setup only: set the project's bearer-link opt-in. It is on by default,
167
+ * like the platform; pass `false` to model an org that switched it off, and
168
+ * `claims.create({ flow: "bearer" })` is then refused with 403.
169
+ */
170
+ enableBearerLinks(enabled?: boolean): void;
171
+ /**
172
+ * Test setup only: model the platform failing to send a claim link it was
173
+ * asked to deliver (a suppressed quota, a bounced address). `deliver: true`
174
+ * then comes back `delivered: false` with the `claimUrl` still valid, so an
175
+ * app's own fallback delivery can be tested.
176
+ */
177
+ suppressClaimDelivery(enabled?: boolean): void;
178
+ /**
179
+ * Test setup only: allow a claim `returnTo` on this origin. The harness app's
180
+ * own origin is allowed by default; anything else is dropped silently, like
181
+ * the platform's allowed-origins check.
182
+ */
183
+ allowReturnOrigin(origin: string): void;
184
+ /**
185
+ * Test setup only: model the customer finishing the platform-hosted claim —
186
+ * Clerk proof plus their confirmation. Flips the claimed contact to verified,
187
+ * or records `invalidated` (the contact was removed from that profile) or
188
+ * `refused` with `contact_owned` (someone else already proved it). Replaying a
189
+ * finished claim returns the recorded result unchanged.
190
+ *
191
+ * It does NOT attach a login link or move history: the platform's merge core
192
+ * is not modelled here, only the ownership decision the app can observe.
193
+ */
194
+ completeClaim(claimId: string, params: {
195
+ clerkUserId: string;
196
+ }): CapturedIdentityClaim;
197
+ /** Test setup only: model reception or the platform refusing a claim. */
198
+ refuseClaim(claimId: string, reason: string): CapturedIdentityClaim;
129
199
  clear(): void;
130
200
  get count(): number;
131
201
  }