@stratacommerce/stripe-connect 0.1.0 → 0.1.1

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.
@@ -53,7 +53,8 @@ export function createEnrollments(ctx) {
53
53
  if (!parsed.success)
54
54
  throw new AppError('Use a current payment challenge from Hub', 'PAYMENT_CHALLENGE_INVALID', 400);
55
55
  const value = parsed.data, expiry = Date.parse(value.expiresAt);
56
- if (expiry <= Date.now() || expiry > Date.now() + 300000 || !Object.keys(value.publicKeys).length)
56
+ // A challenge lasts at most five minutes on the Hub's clock, which may run a minute ahead of this one.
57
+ if (expiry <= Date.now() || expiry > Date.now() + 300000 + 60000 || !Object.keys(value.publicKeys).length)
57
58
  throw new AppError('Use a current payment challenge from Hub', 'PAYMENT_CHALLENGE_INVALID', 400);
58
59
  assertConnectLink(value.scope, link, ctx.environment);
59
60
  for (const pem of Object.values(value.publicKeys)) {
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ export default defineExtension({
14
14
  manifest: {
15
15
  id: 'stripe-connect',
16
16
  name: 'Stripe Connect',
17
- version: '0.1.0',
17
+ version: '0.1.1',
18
18
  apiVersion: '^1.0.0',
19
19
  category: 'payment',
20
20
  description: 'Card payments through Strata Hub’s Stripe Connect platform. Needs a store linked to Strata Hub.',
@@ -5,7 +5,9 @@ export declare class ConnectHubClient {
5
5
  readonly origin: string;
6
6
  private token;
7
7
  private transport;
8
- constructor(origin: string, token: string, transport?: typeof fetch);
8
+ private revoked?;
9
+ /** `revoked` runs when the Hub no longer knows the token, as after a disconnect in the Hub. */
10
+ constructor(origin: string, token: string, transport?: typeof fetch, revoked?: (() => Promise<unknown>) | undefined);
9
11
  request(path: string, body?: unknown): Promise<unknown>;
10
12
  }
11
13
  export interface ConnectServices {
package/dist/provider.js CHANGED
@@ -1,40 +1,72 @@
1
1
  import { isDeepStrictEqual } from 'node:util';
2
2
  import { and, eq } from 'drizzle-orm';
3
+ import { z } from 'zod';
3
4
  import { AppError } from '@stratacommerce/extension-sdk';
4
5
  import { connectConfigurationSchema, connectFactsPageSchema, connectFactsAckResultSchema, CONNECT_API_BASE, CONNECT_API_PATHS, connectOperationResultSchema, connectPath } from '@stratacommerce/hub-protocol';
5
6
  import { CONNECT_PROVIDER, connectIdentity, connectScopeOf } from './identity.js';
6
7
  import { enrollments, financialFacts } from './schema.js';
8
+ // The Hub's payment error codes pass through to the admin and storefront.
9
+ // Its messages don't, so nothing the Hub echoes reaches them.
10
+ const hubErrorSchema = z.object({ error: z.object({ code: z.string().regex(/^PAYMENT_[A-Z_]{1,64}$/) }) });
11
+ async function hubError(response) {
12
+ const bytes = await response.text().catch(() => '');
13
+ try {
14
+ return bytes.length <= 16384 ? hubErrorSchema.safeParse(JSON.parse(bytes)).data?.error.code : undefined;
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
7
20
  // Core never calls Stripe. The Hub holds the platform key; this provider
8
21
  // calls the Hub's Connect API with the enrollment's payment client token.
9
22
  export class ConnectHubClient {
10
23
  origin;
11
24
  token;
12
25
  transport;
13
- constructor(origin, token, transport = fetch) {
26
+ revoked;
27
+ /** `revoked` runs when the Hub no longer knows the token, as after a disconnect in the Hub. */
28
+ constructor(origin, token, transport = fetch, revoked) {
14
29
  this.origin = origin;
15
30
  this.token = token;
16
31
  this.transport = transport;
32
+ this.revoked = revoked;
17
33
  }
18
34
  async request(path, body) {
19
35
  if (!path.startsWith(`${CONNECT_API_BASE}/`) || !/^[a-zA-Z0-9/?=&%_.-]+$/.test(path) || new URL(path, this.origin).origin !== this.origin)
20
36
  throw new AppError('Invalid payment service path', 'PAYMENT_SCOPE_INVALID', 400);
37
+ let response;
21
38
  try {
22
- const response = await this.transport(`${this.origin}${path}`, { method: body === undefined ? 'GET' : 'POST', headers: { Authorization: `Bearer ${this.token}`, ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) },
39
+ response = await this.transport(`${this.origin}${path}`, { method: body === undefined ? 'GET' : 'POST', headers: { Authorization: `Bearer ${this.token}`, ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) },
23
40
  ...(body === undefined ? {} : { body: JSON.stringify(body) }), redirect: 'error', cache: 'no-store', signal: AbortSignal.timeout(15000) });
24
- if (!response.ok)
25
- throw new AppError('Strata payment service is unavailable; retry this payment operation', response.status === 409 ? 'PAYMENT_AUTHORIZATION_UNAVAILABLE' : 'PAYMENT_UNAVAILABLE', response.status >= 500 ? 503 : 409);
41
+ }
42
+ catch {
43
+ throw new AppError('Strata payment service is unavailable; retry this payment operation', 'PAYMENT_UNAVAILABLE', 503);
44
+ }
45
+ if (!response.ok) {
46
+ const code = await hubError(response), retry = response.status >= 500 || response.status === 429;
47
+ if (response.status === 401 && code === 'PAYMENT_CLIENT_INVALID') {
48
+ await this.revoked?.();
49
+ throw new AppError('Strata Hub disconnected this payment account. Reconnect payments in Strata Hub.', 'PAYMENT_AUTHORIZATION_REVOKED', 409);
50
+ }
51
+ if (retry)
52
+ throw new AppError('Strata payment service is unavailable; retry this payment operation', code ?? 'PAYMENT_UNAVAILABLE', 503);
53
+ throw new AppError('Strata payment service refused this payment operation', code ?? (response.status === 409 ? 'PAYMENT_AUTHORIZATION_UNAVAILABLE' : 'PAYMENT_UNAVAILABLE'), 409);
54
+ }
55
+ try {
26
56
  const bytes = await response.text();
27
57
  if (bytes.length > 1048576)
28
58
  throw Error();
29
59
  return JSON.parse(bytes);
30
60
  }
31
- catch (error) {
32
- if (error instanceof AppError)
33
- throw error;
61
+ catch {
34
62
  throw new AppError('Strata payment service is unavailable; retry this payment operation', 'PAYMENT_UNAVAILABLE', 503);
35
63
  }
36
64
  }
37
65
  }
66
+ /** A Hub client with an enrollment's credentials. When the Hub has revoked the client, core revokes the enrollment too. */
67
+ function enrolledClient(enrollments, enrollment) {
68
+ return new ConnectHubClient(enrollment.scope.hubOrigin, enrollment.token, fetch, () => enrollments.revoke(enrollment.id));
69
+ }
38
70
  export class ConnectPaymentProvider {
39
71
  services;
40
72
  identity;
@@ -58,7 +90,7 @@ export class ConnectPaymentProvider {
58
90
  const enrollment = await this.services.enrollments.credentials(this.recipientId);
59
91
  if (enrollment.id !== this.identity.connectionId && !enrollment.recoveryBindingIds.includes(this.identity.connectionId))
60
92
  throw new AppError('Historical payment recovery was not approved', 'PAYMENT_SCOPE_INVALID', 409);
61
- return new ConnectHubClient(enrollment.scope.hubOrigin, enrollment.token);
93
+ return enrolledClient(this.services.enrollments, enrollment);
62
94
  }
63
95
  async original(paymentIntentId) {
64
96
  const rows = await this.services.ctx.payments.attemptsForSession(paymentIntentId);
@@ -162,7 +194,7 @@ export function connectRegistration(services) {
162
194
  return null;
163
195
  const scope = connectScopeOf(provider.identity);
164
196
  const enrollment = await services.enrollments.credentials(provider.identity.connectionId);
165
- const publicConfig = connectConfigurationSchema.safeParse(await new ConnectHubClient(enrollment.scope.hubOrigin, enrollment.token).request(CONNECT_API_PATHS.configuration));
197
+ const publicConfig = connectConfigurationSchema.safeParse(await enrolledClient(services.enrollments, enrollment).request(CONNECT_API_PATHS.configuration));
166
198
  if (!publicConfig.success || !isDeepStrictEqual(publicConfig.data.scope, scope))
167
199
  return null;
168
200
  return { publishableKey: publicConfig.data.publishableKey, stripeAccount: provider.accountId, livemode: scope.livemode };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stratacommerce/stripe-connect",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Card payments through Strata Hub’s Stripe Connect platform, as a StrataCommerce payment extension.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,8 +36,8 @@
36
36
  "@stripe/react-stripe-js": "^6.1.0",
37
37
  "@stripe/stripe-js": "^9.1.0",
38
38
  "zod": "^3",
39
- "@stratacommerce/extension-sdk": "^1.0.0",
40
- "@stratacommerce/hub-protocol": "^0.1.0"
39
+ "@stratacommerce/hub-protocol": "^0.1.0",
40
+ "@stratacommerce/extension-sdk": "^1.0.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "drizzle-orm": ">=0.39 <1",