@rdlabo/workers-hono-kit 0.6.8 → 0.6.10

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.
@@ -40,6 +40,18 @@ export interface AppleVerifyReceiptResponse {
40
40
  * - `unknown` — indeterminate (no receipt, or expired with auto-renew on and no retry) → no-op.
41
41
  */
42
42
  export type AppleRenewalState = 'billing_retry' | 'lapsed' | 'active' | 'unknown';
43
+ /** Apple renewal classification plus the exact provider fields used to reach it. */
44
+ export interface AppleRenewalClassification {
45
+ state: AppleRenewalState;
46
+ originalTransactionId?: string;
47
+ expiresDateMs?: string;
48
+ /** Apple verifyReceipt response status (normally 0 for a successfully verified receipt). */
49
+ statusCode?: number;
50
+ /** Value from the pending-renewal entry matched to `originalTransactionId`. */
51
+ billingRetryStatus?: string;
52
+ /** Value from the pending-renewal entry matched to `originalTransactionId`. */
53
+ autoRenewStatus?: string;
54
+ }
43
55
  /**
44
56
  * Classify an Apple verifyReceipt response into an {@link AppleRenewalState}.
45
57
  *
@@ -52,11 +64,7 @@ export type AppleRenewalState = 'billing_retry' | 'lapsed' | 'active' | 'unknown
52
64
  * @param now - Current epoch-ms (`Date.now()`); injected for determinism/testing.
53
65
  * @returns The state plus the latest `original_transaction_id` / `expires_date_ms` (for the row key).
54
66
  */
55
- export declare function classifyAppleRenewal(verify: unknown, now: number): {
56
- state: AppleRenewalState;
57
- originalTransactionId?: string;
58
- expiresDateMs?: string;
59
- };
67
+ export declare function classifyAppleRenewal(verify: unknown, now: number): AppleRenewalClassification;
60
68
  /**
61
69
  * Verify an App Store receipt against Apple, falling back from production to sandbox.
62
70
  *
package/dist/iap/apple.js CHANGED
@@ -23,6 +23,7 @@ const toArray = (v) => (Array.isArray(v) ? v : []);
23
23
  */
24
24
  export function classifyAppleRenewal(verify, now) {
25
25
  const v = asRecord(verify);
26
+ const statusCode = typeof v?.status === 'number' ? v.status : undefined;
26
27
  // 消耗型など expires_date_ms 欠損エントリを除外してから最新(max expires)を選ぶ。
27
28
  // 欠損値を Number() すると NaN で比較が常に false になり、先頭の欠損行が最後まで残って
28
29
  // 誤って 'active'(isExpired=false)を返すため、有効な expires を持つ行だけを対象にする。
@@ -35,7 +36,7 @@ export function classifyAppleRenewal(verify, now) {
35
36
  }, undefined);
36
37
  const originalTransactionId = latest?.original_transaction_id;
37
38
  if (!latest || !originalTransactionId) {
38
- return { state: 'unknown' };
39
+ return { state: 'unknown', statusCode };
39
40
  }
40
41
  // 複数サブスク商品時に別商品の renewal info を読まないよう、latest と同じ original_transaction_id の
41
42
  // pending エントリを優先(無ければ先頭にフォールバック)。
@@ -43,7 +44,13 @@ export function classifyAppleRenewal(verify, now) {
43
44
  const pending = pendingArr.find((p) => p.original_transaction_id === originalTransactionId) ?? pendingArr.at(0);
44
45
  const expiresMs = Number(latest.expires_date_ms);
45
46
  const isExpired = Number.isFinite(expiresMs) && expiresMs < now;
46
- const base = { originalTransactionId, expiresDateMs: latest.expires_date_ms };
47
+ const base = {
48
+ originalTransactionId,
49
+ expiresDateMs: latest.expires_date_ms,
50
+ statusCode,
51
+ billingRetryStatus: pending?.is_in_billing_retry_period,
52
+ autoRenewStatus: pending?.auto_renew_status,
53
+ };
47
54
  if (pending?.is_in_billing_retry_period === '1') {
48
55
  return { state: 'billing_retry', ...base };
49
56
  }
@@ -36,15 +36,21 @@ export interface GoogleSubscriptionPurchase {
36
36
  * a member is semi-breaking for exhaustive `switch` consumers).
37
37
  */
38
38
  export type GoogleSubscriptionState = 'canceled' | 'gone' | 'active' | 'unknown';
39
+ /** Google subscription classification plus provider diagnostic codes used by persistence. */
40
+ export interface GoogleSubscriptionClassification {
41
+ state: GoogleSubscriptionState;
42
+ /** Google error-body code, such as 410. */
43
+ statusCode?: number;
44
+ /** Google cancellation reason (0=user, 1=system, 2=replaced, 3=developer). */
45
+ cancelReason?: number;
46
+ }
39
47
  /**
40
48
  * Classify a Google subscriptions.get response into a {@link GoogleSubscriptionState}.
41
49
  *
42
50
  * @param purchase - The response body (duck-typed); may be `{ error: { code } }`.
43
51
  * @param now - Current epoch-ms (`Date.now()`); injected for determinism/testing.
44
52
  */
45
- export declare function classifyGoogleSubscription(purchase: unknown, now: number): {
46
- state: GoogleSubscriptionState;
47
- };
53
+ export declare function classifyGoogleSubscription(purchase: unknown, now: number): GoogleSubscriptionClassification;
48
54
  /** OAuth service-account credentials for the Android Publisher API (per-app; inject, never hardcode in kit). */
49
55
  export interface GoogleOAuthCredentials {
50
56
  client_id: string;
@@ -22,18 +22,21 @@ export function classifyGoogleSubscription(purchase, now) {
22
22
  const err = asRecord(p.error);
23
23
  if (err) {
24
24
  // 410 = "The subscription purchase is no longer available for query" (expired too long) = churned.
25
- return { state: err.code === 410 ? 'gone' : 'unknown' };
25
+ const statusCode = typeof err.code === 'number' ? err.code : undefined;
26
+ return { state: statusCode === 410 ? 'gone' : 'unknown', statusCode };
26
27
  }
27
28
  const expiryMs = Number(p.expiryTimeMillis);
28
- const isExpired = Number.isFinite(expiryMs) && expiryMs < now;
29
- const willNotRenew = p.autoRenewing === false || p.cancelReason !== undefined;
29
+ const hasValidExpiry = Number.isFinite(expiryMs);
30
+ const isExpired = hasValidExpiry && expiryMs < now;
31
+ const cancelReason = typeof p.cancelReason === 'number' ? p.cancelReason : undefined;
32
+ const willNotRenew = p.autoRenewing === false || cancelReason !== undefined;
30
33
  if (isExpired && willNotRenew) {
31
- return { state: 'canceled' };
34
+ return { state: 'canceled', cancelReason };
32
35
  }
33
- if (!isExpired) {
34
- return { state: 'active' };
36
+ if (hasValidExpiry && !isExpired) {
37
+ return { state: 'active', cancelReason };
35
38
  }
36
- return { state: 'unknown' };
39
+ return { state: 'unknown', cancelReason };
37
40
  }
38
41
  /**
39
42
  * Exchange a refresh token for an Android Publisher access token.
package/dist/index.d.ts CHANGED
@@ -48,15 +48,15 @@ export type { KVNamespace, KVCacheOptions } from './cache/kv-cache.js';
48
48
  export { createStripeClient, verifyStripeWebhook } from './stripe/client.js';
49
49
  export type { CreateStripeClientOptions } from './stripe/client.js';
50
50
  export { extractStripeFailureReason, stripeFailureMessageJa, serializePaymentFailure, parsePaymentFailure, PaymentDeclinedError, toPaymentDeclinedError, } from './stripe/failure.js';
51
- export type { StripeFailureReason, PaymentFailureSource, PaymentFailureRecord, PaymentDeclinedBody, } from './stripe/failure.js';
51
+ export type { StripeFailureReason, PaymentFailureSource, PaymentFailureRecord, PaymentFailureReason, IapFailureReason, PaymentDeclinedBody, } from './stripe/failure.js';
52
52
  export { classifyStripeReconcile } from './stripe/reconcile.js';
53
53
  export type { StripeReconcileAction } from './stripe/reconcile.js';
54
54
  export { paymentFailureMessageJa, iapFailureKey, UNRESOLVED_PAYMENT_STATUSES } from './payment/failure.js';
55
55
  export type { PaymentFailureStatus, PaymentFailureType } from './payment/failure.js';
56
56
  export { classifyAppleRenewal, verifyAppleReceipt } from './iap/apple.js';
57
- export type { AppleRenewalState, AppleVerifyReceiptResponse, ApplePendingRenewalInfo, AppleLatestReceiptInfo, } from './iap/apple.js';
57
+ export type { AppleRenewalClassification, AppleRenewalState, AppleVerifyReceiptResponse, ApplePendingRenewalInfo, AppleLatestReceiptInfo, } from './iap/apple.js';
58
58
  export { classifyGoogleSubscription, getGoogleSubscription, googleAccessToken } from './iap/google.js';
59
- export type { GoogleSubscriptionState, GoogleSubscriptionPurchase, GoogleOAuthCredentials } from './iap/google.js';
59
+ export type { GoogleSubscriptionClassification, GoogleSubscriptionState, GoogleSubscriptionPurchase, GoogleOAuthCredentials, } from './iap/google.js';
60
60
  export { retryWhenDeadlock } from './db/retry.js';
61
61
  export { sendInChunks } from './queue/send.js';
62
62
  export type { QueueLike, QueueSendMessage } from './queue/send.js';
@@ -1,4 +1,4 @@
1
- import type { StripeFailureReason } from '../stripe/failure.js';
1
+ import type { PaymentFailureReason } from '../stripe/failure.js';
2
2
  /**
3
3
  * Provider-agnostic status of a row in the `payment_failed` table.
4
4
  *
@@ -35,22 +35,23 @@ export declare function paymentFailureMessageJa(input: {
35
35
  status: PaymentFailureStatus;
36
36
  /** The row `type` (stripe/ios/android). Typed as `string` since it comes from the DB column. */
37
37
  type?: string | null;
38
- reason?: StripeFailureReason | null;
38
+ reason?: PaymentFailureReason | null;
39
39
  }): string;
40
40
  /**
41
- * Build the `payment_failed.recursions_id` (PRIMARY KEY) with a per-type namespace.
41
+ * Build the provider-native `payment_failed.recursions_id`.
42
42
  *
43
43
  * @remarks
44
- * - iOS: `ios:${original_transaction_id}:${expires_date_ms}`. `original_transaction_id` is stable
44
+ * - iOS: `${original_transaction_id}:${expires_date_ms}`. `original_transaction_id` is stable
45
45
  * across re-subscribes, so keying on it alone would let the resolved-reopen guard permanently mask
46
46
  * every event after the first. Including `expires_date_ms` makes each billing cycle a distinct row,
47
47
  * while the same cycle's daily reconcile (billing-retry) still converges to one row.
48
- * - Android: `android:${orderId}` — Google issues a new orderId per subscription, so it is already
49
- * cycle-specific; the `android:` prefix only guards against cross-type PK collisions.
48
+ * - Android: `${orderId}` — Google issues a new orderId per subscription, so it is already cycle-specific.
50
49
  *
51
50
  * Stripe rows use the invoice / subscription id directly (globally unique) and need no helper.
52
51
  *
53
- * @remarks Fits the fleet's `payment_failed.recursions_id` `varchar(50)` (iOS ~38, Android ~36 chars).
52
+ * The provider is stored separately in the `type` column. Provider-native ID formats are disjoint in practice
53
+ * (Apple numeric pair / Google `GPA.*` / Stripe `in_*` or `sub_*`), so duplicating `type` in the primary-key value
54
+ * is unnecessary.
54
55
  */
55
56
  export declare function iapFailureKey(input: {
56
57
  platform: 'ios';
@@ -32,25 +32,25 @@ export function paymentFailureMessageJa(input) {
32
32
  if (input.status === 'failed' && (input.type === 'ios' || input.type === 'android')) {
33
33
  return IAP_FAILED_MESSAGE_JA[input.type];
34
34
  }
35
- return stripeFailureMessageJa(input.reason ?? null);
35
+ const stripeReason = input.reason && !('provider' in input.reason) ? input.reason : null;
36
+ return stripeFailureMessageJa(stripeReason);
36
37
  }
37
38
  /**
38
- * Build the `payment_failed.recursions_id` (PRIMARY KEY) with a per-type namespace.
39
+ * Build the provider-native `payment_failed.recursions_id`.
39
40
  *
40
41
  * @remarks
41
- * - iOS: `ios:${original_transaction_id}:${expires_date_ms}`. `original_transaction_id` is stable
42
+ * - iOS: `${original_transaction_id}:${expires_date_ms}`. `original_transaction_id` is stable
42
43
  * across re-subscribes, so keying on it alone would let the resolved-reopen guard permanently mask
43
44
  * every event after the first. Including `expires_date_ms` makes each billing cycle a distinct row,
44
45
  * while the same cycle's daily reconcile (billing-retry) still converges to one row.
45
- * - Android: `android:${orderId}` — Google issues a new orderId per subscription, so it is already
46
- * cycle-specific; the `android:` prefix only guards against cross-type PK collisions.
46
+ * - Android: `${orderId}` — Google issues a new orderId per subscription, so it is already cycle-specific.
47
47
  *
48
48
  * Stripe rows use the invoice / subscription id directly (globally unique) and need no helper.
49
49
  *
50
- * @remarks Fits the fleet's `payment_failed.recursions_id` `varchar(50)` (iOS ~38, Android ~36 chars).
50
+ * The provider is stored separately in the `type` column. Provider-native ID formats are disjoint in practice
51
+ * (Apple numeric pair / Google `GPA.*` / Stripe `in_*` or `sub_*`), so duplicating `type` in the primary-key value
52
+ * is unnecessary.
51
53
  */
52
54
  export function iapFailureKey(input) {
53
- return input.platform === 'ios'
54
- ? `ios:${input.originalTransactionId}:${input.expiresDateMs}`
55
- : `android:${input.orderId}`;
55
+ return input.platform === 'ios' ? `${input.originalTransactionId}:${input.expiresDateMs}` : input.orderId;
56
56
  }
@@ -22,6 +22,23 @@ export interface StripeFailureReason {
22
22
  /** Id of the Subscription the failing invoice belongs to, when resolvable. */
23
23
  subscriptionId?: string;
24
24
  }
25
+ /** Normalized reason persisted for an App Store / Google Play subscription failure. */
26
+ export interface IapFailureReason {
27
+ /** Provider discriminator. */
28
+ provider: 'ios' | 'android';
29
+ /** Stable machine-readable classification. */
30
+ code: 'billing_retry' | 'auto_renew_off' | 'subscription_canceled' | 'subscription_gone';
31
+ /** Provider response status code when present (Apple verifyReceipt status / Google error code). */
32
+ statusCode?: number;
33
+ /** Apple auto-renew status (`'0'` means disabled). */
34
+ autoRenewStatus?: string;
35
+ /** Apple billing-retry status (`'1'` means retrying a failed renewal). */
36
+ billingRetryStatus?: string;
37
+ /** Google cancellation reason code (0=user, 1=system, 2=replaced, 3=developer). */
38
+ cancelReason?: number;
39
+ }
40
+ /** Provider-specific diagnostic reason stored in `payment_failed.receipt`. */
41
+ export type PaymentFailureReason = StripeFailureReason | IapFailureReason;
25
42
  /** Where a {@link PaymentFailureRecord} was captured. */
26
43
  export type PaymentFailureSource = 'webhook.invoice.payment_failed' | 'webhook.invoice.payment_action_required' | 'reconcile' | 'checkout' | 'iap';
27
44
  /**
@@ -32,7 +49,7 @@ export type PaymentFailureSource = 'webhook.invoice.payment_failed' | 'webhook.i
32
49
  * on read via {@link stripeFailureMessageJa} so that wording changes never require a data migration.
33
50
  */
34
51
  export interface PaymentFailureRecord {
35
- reason: StripeFailureReason;
52
+ reason: PaymentFailureReason;
36
53
  source: PaymentFailureSource;
37
54
  /** ISO 8601 timestamp of when the failure was captured. */
38
55
  occurredAt: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rdlabo/workers-hono-kit",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"