adapty 0.8.5 → 0.8.7

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 (116) hide show
  1. package/README.md +7 -1
  2. package/dist/cli/base/adapty/adapty-command.d.ts +21 -0
  3. package/dist/cli/base/adapty/adapty-command.js +47 -0
  4. package/dist/cli/base/adapty/build.d.ts +11 -0
  5. package/dist/cli/base/adapty/build.js +13 -0
  6. package/dist/cli/base/adapty/index.d.ts +5 -0
  7. package/dist/cli/base/adapty/index.js +3 -0
  8. package/dist/cli/base/adapty/openSession.d.ts +22 -0
  9. package/dist/cli/base/adapty/openSession.js +25 -0
  10. package/dist/cli/base/base-command.d.ts +30 -0
  11. package/dist/cli/base/base-command.js +64 -0
  12. package/dist/cli/commands/apps/create.d.ts +19 -0
  13. package/dist/cli/commands/apps/create.js +68 -0
  14. package/dist/cli/commands/apps/get.d.ts +10 -0
  15. package/dist/cli/commands/apps/get.js +14 -0
  16. package/dist/cli/commands/apps/list.d.ts +11 -0
  17. package/dist/cli/commands/apps/list.js +14 -0
  18. package/dist/cli/commands/apps/update.d.ts +15 -0
  19. package/dist/cli/commands/apps/update.js +30 -0
  20. package/dist/cli/commands/auth/login.d.ts +11 -0
  21. package/dist/cli/commands/auth/login.js +68 -0
  22. package/dist/cli/commands/auth/logout.d.ts +12 -0
  23. package/dist/cli/commands/auth/logout.js +28 -0
  24. package/dist/cli/commands/auth/revoke.d.ts +13 -0
  25. package/dist/cli/commands/auth/revoke.js +32 -0
  26. package/dist/cli/commands/auth/status/index.d.ts +7 -0
  27. package/dist/cli/commands/auth/status/index.js +22 -0
  28. package/dist/cli/commands/auth/status/lib/render.d.ts +3 -0
  29. package/dist/cli/commands/auth/status/lib/render.js +14 -0
  30. package/dist/cli/commands/auth/status/lib/result.d.ts +15 -0
  31. package/dist/cli/commands/auth/status/lib/result.js +1 -0
  32. package/dist/cli/commands/auth/whoami.d.ts +10 -0
  33. package/dist/cli/commands/auth/whoami.js +16 -0
  34. package/dist/cli/errors.d.ts +39 -0
  35. package/dist/cli/errors.js +92 -0
  36. package/dist/cli/flags.d.ts +16 -0
  37. package/dist/cli/flags.js +27 -0
  38. package/dist/cli/views/list.d.ts +6 -0
  39. package/dist/cli/views/list.js +10 -0
  40. package/dist/cli/views/record.d.ts +9 -0
  41. package/dist/cli/views/record.js +14 -0
  42. package/dist/commands/apps/create.d.ts +1 -14
  43. package/dist/commands/apps/create.js +2 -57
  44. package/dist/commands/apps/get.d.ts +1 -11
  45. package/dist/commands/apps/get.js +2 -22
  46. package/dist/commands/apps/list.d.ts +1 -13
  47. package/dist/commands/apps/list.js +2 -19
  48. package/dist/commands/apps/update.d.ts +1 -16
  49. package/dist/commands/apps/update.js +2 -41
  50. package/dist/commands/asa/keywords/recommend.d.ts +20 -0
  51. package/dist/commands/asa/keywords/recommend.js +102 -0
  52. package/dist/commands/asa/whoami.js +6 -2
  53. package/dist/commands/auth/login.d.ts +1 -8
  54. package/dist/commands/auth/login.js +2 -122
  55. package/dist/commands/auth/logout.d.ts +1 -9
  56. package/dist/commands/auth/logout.js +2 -18
  57. package/dist/commands/auth/revoke.d.ts +1 -9
  58. package/dist/commands/auth/revoke.js +2 -21
  59. package/dist/commands/auth/status.d.ts +1 -7
  60. package/dist/commands/auth/status.js +2 -26
  61. package/dist/commands/auth/whoami.d.ts +1 -7
  62. package/dist/commands/auth/whoami.js +2 -15
  63. package/dist/lib/api-schemas.d.ts +7 -3
  64. package/dist/lib/asa-schemas.d.ts +41 -1
  65. package/dist/sdk/adapty/access-levels.d.ts +15 -0
  66. package/dist/sdk/adapty/access-levels.js +4 -0
  67. package/dist/sdk/adapty/apps/create.d.ts +26 -0
  68. package/dist/sdk/adapty/apps/create.js +36 -0
  69. package/dist/sdk/adapty/apps/index.d.ts +13 -0
  70. package/dist/sdk/adapty/apps/index.js +9 -0
  71. package/dist/sdk/adapty/apps/model.d.ts +24 -0
  72. package/dist/sdk/adapty/apps/model.js +10 -0
  73. package/dist/sdk/adapty/apps/resource.d.ts +19 -0
  74. package/dist/sdk/adapty/apps/resource.js +23 -0
  75. package/dist/sdk/adapty/apps/update.d.ts +16 -0
  76. package/dist/sdk/adapty/apps/update.js +25 -0
  77. package/dist/sdk/adapty/auth/device-code.d.ts +8 -0
  78. package/dist/sdk/adapty/auth/device-code.js +19 -0
  79. package/dist/sdk/adapty/auth/index.d.ts +7 -0
  80. package/dist/sdk/adapty/auth/index.js +5 -0
  81. package/dist/sdk/adapty/auth/model.d.ts +15 -0
  82. package/dist/sdk/adapty/auth/model.js +6 -0
  83. package/dist/sdk/adapty/auth/poll-token.d.ts +8 -0
  84. package/dist/sdk/adapty/auth/poll-token.js +58 -0
  85. package/dist/sdk/adapty/auth/resource.d.ts +17 -0
  86. package/dist/sdk/adapty/auth/resource.js +21 -0
  87. package/dist/sdk/adapty/errors.d.ts +10 -0
  88. package/dist/sdk/adapty/errors.js +38 -0
  89. package/dist/sdk/adapty/index.d.ts +36 -0
  90. package/dist/sdk/adapty/index.js +27 -0
  91. package/dist/sdk/adapty/pagination.d.ts +23 -0
  92. package/dist/sdk/adapty/pagination.js +1 -0
  93. package/dist/sdk/core/auth/device-flow.d.ts +46 -0
  94. package/dist/sdk/core/auth/device-flow.js +62 -0
  95. package/dist/sdk/core/clock.d.ts +7 -0
  96. package/dist/sdk/core/clock.js +21 -0
  97. package/dist/sdk/core/errors.d.ts +81 -0
  98. package/dist/sdk/core/errors.js +84 -0
  99. package/dist/sdk/core/http/client.d.ts +46 -0
  100. package/dist/sdk/core/http/client.js +127 -0
  101. package/dist/sdk/core/http/index.d.ts +11 -0
  102. package/dist/sdk/core/http/index.js +7 -0
  103. package/dist/sdk/core/http/policies.d.ts +14 -0
  104. package/dist/sdk/core/http/policies.js +29 -0
  105. package/dist/sdk/core/http/retry.d.ts +27 -0
  106. package/dist/sdk/core/http/retry.js +22 -0
  107. package/dist/sdk/core/http/url.d.ts +3 -0
  108. package/dist/sdk/core/http/url.js +19 -0
  109. package/dist/sdk/core/session.d.ts +20 -0
  110. package/dist/sdk/core/session.js +69 -0
  111. package/dist/sdk/core/testing.d.ts +25 -0
  112. package/dist/sdk/core/testing.js +46 -0
  113. package/dist/sdk/core/validation.d.ts +10 -0
  114. package/dist/sdk/core/validation.js +14 -0
  115. package/oclif.manifest.json +211 -148
  116. package/package.json +10 -2
@@ -0,0 +1,23 @@
1
+ import { assertValid } from '../../core/validation.js';
2
+ import { pageQuery } from '../pagination.js';
3
+ import { toCreateRequest, validateCreateApp } from './create.js';
4
+ import { toUpdateRequest, validateUpdateApp } from './update.js';
5
+ /**
6
+ * Every path of the apps resource in one place, so the endpoints can be read as a list. What each
7
+ * operation needs of its own — input shape, rules, request body — lives in its own file.
8
+ *
9
+ * The validating methods are async on purpose — a broken rule then arrives as a rejected promise,
10
+ * like a network failure, and one await covers both.
11
+ */
12
+ export const apps = (http) => ({
13
+ get: (appId) => http.get(`/apps/${appId}`),
14
+ list: (params) => http.get('/apps', { query: pageQuery(params) }),
15
+ create: async (input) => {
16
+ assertValid(validateCreateApp(input));
17
+ return http.post('/apps', toCreateRequest(input));
18
+ },
19
+ update: async (appId, input) => {
20
+ assertValid(validateUpdateApp(input));
21
+ return http.put(`/apps/${appId}`, toUpdateRequest(input));
22
+ },
23
+ });
@@ -0,0 +1,16 @@
1
+ import type { Issue } from '../../core/errors.js';
2
+ export type UpdateAppInput = {
3
+ appleBundleId?: string | undefined;
4
+ googleBundleId?: string | undefined;
5
+ title?: string | undefined;
6
+ };
7
+ type UpdateAppRequest = {
8
+ apple_bundle_id?: string;
9
+ google_bundle_id?: string;
10
+ title?: string;
11
+ };
12
+ /** Same contract as the create rule: problems as a list, paths naming input fields. */
13
+ export declare const validateUpdateApp: (input: UpdateAppInput) => Issue[];
14
+ /** An absent field is left out of the body: the server tells "not given" from "set to empty". */
15
+ export declare const toUpdateRequest: (input: UpdateAppInput) => UpdateAppRequest;
16
+ export {};
@@ -0,0 +1,25 @@
1
+ /** Same contract as the create rule: problems as a list, paths naming input fields. */
2
+ export const validateUpdateApp = (input) => {
3
+ const fields = [input.title, input.appleBundleId, input.googleBundleId];
4
+ if (fields.every(value => value === undefined)) {
5
+ return [{ message: 'nothing to update: pass a title or a bundle id' }];
6
+ }
7
+ if (input.title?.trim() === '') {
8
+ return [{ message: 'must not be empty', path: 'title' }];
9
+ }
10
+ return [];
11
+ };
12
+ /** An absent field is left out of the body: the server tells "not given" from "set to empty". */
13
+ export const toUpdateRequest = (input) => {
14
+ const body = {};
15
+ if (input.title !== undefined) {
16
+ body.title = input.title;
17
+ }
18
+ if (input.appleBundleId !== undefined) {
19
+ body.apple_bundle_id = input.appleBundleId;
20
+ }
21
+ if (input.googleBundleId !== undefined) {
22
+ body.google_bundle_id = input.googleBundleId;
23
+ }
24
+ return body;
25
+ };
@@ -0,0 +1,8 @@
1
+ import type { DeviceCode } from '../../core/auth/device-flow.js';
2
+ import type { Http } from '../../core/http/index.js';
3
+ /**
4
+ * Step one of the device flow: ask for the code the user will type. The path sits with the
5
+ * reading of the answer, because here they are one thing — what core's port receives is this
6
+ * server's response renamed, and the two cannot be understood apart.
7
+ */
8
+ export declare const requestDeviceCode: (http: Http) => Promise<DeviceCode>;
@@ -0,0 +1,19 @@
1
+ import { CLIENT_ID } from './model.js';
2
+ /**
3
+ * Step one of the device flow: ask for the code the user will type. The path sits with the
4
+ * reading of the answer, because here they are one thing — what core's port receives is this
5
+ * server's response renamed, and the two cannot be understood apart.
6
+ */
7
+ export const requestDeviceCode = async (http) => {
8
+ const response = await http.post('/auth/device', { client_id: CLIENT_ID });
9
+ return {
10
+ deviceCode: response.device_code,
11
+ expiresInSec: response.expires_in,
12
+ // optional in the protocol; the default is knowledge about this server, so it
13
+ // belongs here and not in the orchestration
14
+ intervalSec: response.interval_seconds ?? 5,
15
+ userCode: response.user_code,
16
+ verificationUri: response.verification_uri,
17
+ verificationUriComplete: response.verification_uri_complete,
18
+ };
19
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The door of the auth resource: re-exports only, no code of its own. The two device flow steps
3
+ * are reached through the port `auth()` returns, not imported directly.
4
+ */
5
+ export { auth } from './resource.js';
6
+ export type { AuthUser, IssuedToken } from './model.js';
7
+ export type { AuthApi } from './resource.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The door of the auth resource: re-exports only, no code of its own. The two device flow steps
3
+ * are reached through the port `auth()` returns, not imported directly.
4
+ */
5
+ export { auth } from './resource.js';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * What the auth operations share: the shapes a successful login yields, and the name this client
3
+ * gives itself. An input shape belongs to its operation, not here.
4
+ */
5
+ export type AuthUser = {
6
+ email: string;
7
+ name: string;
8
+ };
9
+ export type IssuedToken = {
10
+ accessToken: string;
11
+ expiresInSec: number;
12
+ user: AuthUser;
13
+ };
14
+ /** The device flow identifies the client rather than authenticating it: there is no secret. */
15
+ export declare const CLIENT_ID = "adapty-cli";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * What the auth operations share: the shapes a successful login yields, and the name this client
3
+ * gives itself. An input shape belongs to its operation, not here.
4
+ */
5
+ /** The device flow identifies the client rather than authenticating it: there is no secret. */
6
+ export const CLIENT_ID = 'adapty-cli';
@@ -0,0 +1,8 @@
1
+ import type { IssuedToken } from './model.js';
2
+ import type { PollResult } from '../../core/auth/device-flow.js';
3
+ import type { Http } from '../../core/http/index.js';
4
+ /**
5
+ * Step two, asked over and over until the user is done: the answer is a token, a state of the
6
+ * protocol, or a failure — and telling the three apart is the whole of this operation.
7
+ */
8
+ export declare const pollToken: (http: Http, deviceCode: string) => Promise<PollResult<IssuedToken>>;
@@ -0,0 +1,58 @@
1
+ import { ApiError } from '../../core/errors.js';
2
+ import { CLIENT_ID } from './model.js';
3
+ /** A 400 carrying one of these codes is a state of the protocol, not a failure. */
4
+ const pollResultByCode = {
5
+ access_denied: { status: 'denied' },
6
+ authorization_pending: { status: 'pending' },
7
+ expired_token: { status: 'expired' },
8
+ slow_down: { status: 'slow_down' },
9
+ };
10
+ /** A protocol code as a state, or a real error when the code is not one of the protocol's. */
11
+ const toPollResult = (code, body) => {
12
+ const result = pollResultByCode[code];
13
+ if (result === undefined) {
14
+ throw new ApiError({
15
+ code,
16
+ details: body,
17
+ message: `Unexpected device flow response: ${code}`,
18
+ status: 200,
19
+ });
20
+ }
21
+ return result;
22
+ };
23
+ /**
24
+ * Step two, asked over and over until the user is done: the answer is a token, a state of the
25
+ * protocol, or a failure — and telling the three apart is the whole of this operation.
26
+ */
27
+ export const pollToken = async (http, deviceCode) => {
28
+ try {
29
+ const response = await http.post('/auth/token', {
30
+ client_id: CLIENT_ID,
31
+ device_code: deviceCode,
32
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
33
+ });
34
+ // A protocol state normally arrives as a 400 and lands in the catch below, but the
35
+ // shipped CLI's types also anticipate a 200 whose body is { error: <code> }, and
36
+ // reading only the thrown case would call a pending login authorized.
37
+ if (!('access_token' in response)) {
38
+ return toPollResult(response.error, response);
39
+ }
40
+ return {
41
+ status: 'authorized',
42
+ token: {
43
+ accessToken: response.access_token,
44
+ expiresInSec: response.expires_in,
45
+ user: response.user,
46
+ },
47
+ };
48
+ }
49
+ catch (error) {
50
+ if (error instanceof ApiError && error.code !== undefined) {
51
+ const result = pollResultByCode[error.code];
52
+ if (result) {
53
+ return result;
54
+ }
55
+ }
56
+ throw error;
57
+ }
58
+ };
@@ -0,0 +1,17 @@
1
+ import type { IssuedToken } from './model.js';
2
+ import type { Http } from '../../core/http/index.js';
3
+ /**
4
+ * The auth resource: core's DeviceAuthApi port filled in by the two device flow steps, plus the
5
+ * endpoints that only need a path. A missing or stale token surfaces as AuthRequiredError.
6
+ *
7
+ * Unlike apps, not every path is listed here: a device flow step is its path and the reading of
8
+ * its answer together, so each keeps its own. What is left in this file is the shape of the
9
+ * resource — which port it satisfies, and what else it answers.
10
+ */
11
+ export declare const auth: (http: Http) => {
12
+ me: () => Promise<Record<string, unknown>>;
13
+ revokeToken: (token: string) => Promise<undefined>;
14
+ pollToken(deviceCode: string): Promise<import("../../core/auth/device-flow.js").PollResult<IssuedToken>>;
15
+ requestDeviceCode(): Promise<import("../../core/auth/device-flow.js").DeviceCode>;
16
+ };
17
+ export type AuthApi = ReturnType<typeof auth>;
@@ -0,0 +1,21 @@
1
+ import { requestDeviceCode } from './device-code.js';
2
+ import { pollToken } from './poll-token.js';
3
+ /**
4
+ * The auth resource: core's DeviceAuthApi port filled in by the two device flow steps, plus the
5
+ * endpoints that only need a path. A missing or stale token surfaces as AuthRequiredError.
6
+ *
7
+ * Unlike apps, not every path is listed here: a device flow step is its path and the reading of
8
+ * its answer together, so each keeps its own. What is left in this file is the shape of the
9
+ * resource — which port it satisfies, and what else it answers.
10
+ */
11
+ export const auth = (http) => {
12
+ const deviceAuth = {
13
+ pollToken: (deviceCode) => pollToken(http, deviceCode),
14
+ requestDeviceCode: () => requestDeviceCode(http),
15
+ };
16
+ return {
17
+ ...deviceAuth,
18
+ me: () => http.get('/me'),
19
+ revokeToken: (token) => http.post('/auth/tokens/revoke', { token }),
20
+ };
21
+ };
@@ -0,0 +1,10 @@
1
+ import type { ErrorParser } from '../core/http/index.js';
2
+ /**
3
+ * How this service words a rejection: `{ error_code, errors: { field: [message] } }`, sometimes
4
+ * just `{ error: 'code' }`. Ported from src/lib/errors.ts so a migrated command keeps saying
5
+ * "apple_bundle_id: already used" instead of "POST /apps failed with HTTP 400".
6
+ *
7
+ * Wired once in createAdapty: which shapes a service speaks is product knowledge, and ASA speaks
8
+ * another — which is why the transport takes the parser as a parameter.
9
+ */
10
+ export declare const developerErrorParser: ErrorParser;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * How this service words a rejection: `{ error_code, errors: { field: [message] } }`, sometimes
3
+ * just `{ error: 'code' }`. Ported from src/lib/errors.ts so a migrated command keeps saying
4
+ * "apple_bundle_id: already used" instead of "POST /apps failed with HTTP 400".
5
+ *
6
+ * Wired once in createAdapty: which shapes a service speaks is product knowledge, and ASA speaks
7
+ * another — which is why the transport takes the parser as a parameter.
8
+ */
9
+ export const developerErrorParser = (_status, body) => {
10
+ if (typeof body !== 'object' || body === null) {
11
+ return {};
12
+ }
13
+ const { error, error_code: errorCode, errors } = body;
14
+ if (typeof errorCode === 'string' && errorCode !== '') {
15
+ // The bare code as the fallback message: what the published CLI prints, and still better
16
+ // than an HTTP status when the server sends no field errors
17
+ return { code: errorCode, message: fieldMessages(errors) ?? errorCode };
18
+ }
19
+ if (typeof error === 'string' && error !== '') {
20
+ return { code: error, message: error };
21
+ }
22
+ return {};
23
+ };
24
+ /** `non_field_errors` is the server's name for "about the request as a whole": printed bare. */
25
+ const fieldMessages = (errors) => {
26
+ if (typeof errors !== 'object' || errors === null) {
27
+ return undefined;
28
+ }
29
+ const parts = [];
30
+ for (const [field, messages] of Object.entries(errors)) {
31
+ for (const message of Array.isArray(messages) ? messages : [messages]) {
32
+ if (typeof message === 'string') {
33
+ parts.push(field === 'non_field_errors' ? message : `${field}: ${message}`);
34
+ }
35
+ }
36
+ }
37
+ return parts.length > 0 ? parts.join('; ') : undefined;
38
+ };
@@ -0,0 +1,36 @@
1
+ import type { AccessLevelsApi } from './access-levels.js';
2
+ import type { AppsApi } from './apps/index.js';
3
+ import type { AuthApi } from './auth/index.js';
4
+ import type { Clock } from '../core/clock.js';
5
+ import type { RetryAttempt } from '../core/http/index.js';
6
+ export { platforms } from './apps/index.js';
7
+ export { developerErrorParser } from './errors.js';
8
+ export type { AccessLevel, AccessLevelList, AccessLevelsApi } from './access-levels.js';
9
+ export type { AppDetail, AppsApi, AppSummary, CreateAppInput, UpdateAppInput } from './apps/index.js';
10
+ export type { AuthApi, AuthUser, IssuedToken } from './auth/index.js';
11
+ export type { PageParams, Paginated, Pagination } from './pagination.js';
12
+ /** Exported because the adapter compares the resolved URL with it and warns when they differ. */
13
+ export declare const DEFAULT_ADAPTY_API_URL = "https://api-admin.adapty.io/api/v1/developer";
14
+ /**
15
+ * Narrower than HttpOptions on purpose: the transport seams (parseError, shouldRetry, the retry
16
+ * policy, trailingSlash) are product knowledge, and two adapters overriding them would read the
17
+ * same answers differently. What is left is what a consumer really owns.
18
+ */
19
+ export type AdaptyOptions = {
20
+ baseUrl?: string | undefined;
21
+ clock?: Clock | undefined;
22
+ fetch?: typeof globalThis.fetch | undefined;
23
+ onRetry?: ((info: RetryAttempt) => void) | undefined;
24
+ signal?: AbortSignal | undefined;
25
+ /** Without a token only auth is usable — that is how login builds the sdk. */
26
+ token?: string | undefined;
27
+ /** Comes from the adapter, so server logs can tell the CLI from an MCP server. */
28
+ userAgent?: string | undefined;
29
+ };
30
+ export type Adapty = {
31
+ accessLevels: AccessLevelsApi;
32
+ apps: AppsApi;
33
+ auth: AuthApi;
34
+ };
35
+ /** The assembly point of the developer API: one transport, resources on top of it. */
36
+ export declare const createAdapty: (options?: AdaptyOptions) => Adapty;
@@ -0,0 +1,27 @@
1
+ import { createHttp } from '../core/http/index.js';
2
+ import { accessLevels } from './access-levels.js';
3
+ import { apps } from './apps/index.js';
4
+ import { auth } from './auth/index.js';
5
+ import { developerErrorParser } from './errors.js';
6
+ export { platforms } from './apps/index.js';
7
+ export { developerErrorParser } from './errors.js';
8
+ /** Exported because the adapter compares the resolved URL with it and warns when they differ. */
9
+ export const DEFAULT_ADAPTY_API_URL = 'https://api-admin.adapty.io/api/v1/developer';
10
+ /** The assembly point of the developer API: one transport, resources on top of it. */
11
+ export const createAdapty = (options = {}) => {
12
+ const http = createHttp({
13
+ baseUrl: options.baseUrl ?? DEFAULT_ADAPTY_API_URL,
14
+ clock: options.clock,
15
+ fetch: options.fetch,
16
+ headers: options.userAgent === undefined ? undefined : { 'user-agent': options.userAgent },
17
+ onRetry: options.onRetry,
18
+ parseError: developerErrorParser,
19
+ signal: options.signal,
20
+ token: options.token,
21
+ });
22
+ return {
23
+ accessLevels: accessLevels(http),
24
+ apps: apps(http),
25
+ auth: auth(http),
26
+ };
27
+ };
@@ -0,0 +1,23 @@
1
+ import type { QueryParams } from '../core/http/index.js';
2
+ /**
3
+ * The paginated answer, and the query that asks for a page. Product and not core: `meta.pagination`
4
+ * and the bracketed `page[...]` names are this API's convention, not the transport's business. ASA
5
+ * follows the same convention today, so sdk/asa can import this until the two actually diverge.
6
+ */
7
+ export type Pagination = {
8
+ count: number;
9
+ page: number;
10
+ pages: number;
11
+ };
12
+ export type Paginated<T> = {
13
+ data: T[];
14
+ meta: {
15
+ pagination: Pagination;
16
+ };
17
+ };
18
+ /** Page selection as we take it: camelCase in, the server's names on the wire. */
19
+ export type PageParams = {
20
+ page?: number | undefined;
21
+ pageSize?: number | undefined;
22
+ };
23
+ export declare const pageQuery: (params: PageParams | undefined) => QueryParams;
@@ -0,0 +1 @@
1
+ export const pageQuery = (params) => ({ 'page[number]': params?.page, 'page[size]': params?.pageSize });
@@ -0,0 +1,46 @@
1
+ import type { Clock } from '../clock.js';
2
+ export type DeviceCode = {
3
+ deviceCode: string;
4
+ expiresInSec: number;
5
+ intervalSec: number;
6
+ userCode: string;
7
+ verificationUri: string;
8
+ verificationUriComplete: string | undefined;
9
+ };
10
+ export type PollResult<TToken> = {
11
+ status: 'authorized';
12
+ token: TToken;
13
+ } | {
14
+ status: 'denied';
15
+ } | {
16
+ status: 'expired';
17
+ } | {
18
+ status: 'pending';
19
+ } | {
20
+ status: 'slow_down';
21
+ };
22
+ /**
23
+ * The device flow port (RFC 8628) — all the orchestration below knows. What sits inside a token
24
+ * (for Adapty, an access_token plus the user) is the port's business.
25
+ */
26
+ export type DeviceAuthApi<TToken> = {
27
+ pollToken(deviceCode: string): Promise<PollResult<TToken>>;
28
+ requestDeviceCode(): Promise<DeviceCode>;
29
+ };
30
+ export type DeviceFlowDeps = {
31
+ clock: Clock;
32
+ maxConsecutiveErrors?: number | undefined;
33
+ /** Do not poll faster than this, even if the server allows it. */
34
+ minIntervalMs?: number | undefined;
35
+ /** Show the code and the link to the user. Sdk prints nothing itself. */
36
+ onCode: (code: DeviceCode) => void;
37
+ /** One poll failed (network, odd answer). The flow goes on up to maxConsecutiveErrors. */
38
+ onTransientError?: ((error: unknown, consecutive: number) => void) | undefined;
39
+ signal?: AbortSignal | undefined;
40
+ };
41
+ /**
42
+ * Device flow orchestration with every effect kept outside: the network behind the api port, time
43
+ * behind clock, cancellation behind signal. That is what makes expiry, slow_down, poll failures
44
+ * and Ctrl+C unit-testable.
45
+ */
46
+ export declare const runDeviceFlow: <TToken>(api: DeviceAuthApi<TToken>, deps: DeviceFlowDeps) => Promise<TToken>;
@@ -0,0 +1,62 @@
1
+ import { throwIfAborted } from '../clock.js';
2
+ import { CancelledError, DeviceFlowDeniedError, DeviceFlowExpiredError } from '../errors.js';
3
+ /** RFC 8628, 3.5: on slow_down the interval grows by 5 seconds. */
4
+ const SLOW_DOWN_STEP_MS = 5000;
5
+ const DEFAULT_MIN_INTERVAL_MS = 5000;
6
+ const DEFAULT_MAX_CONSECUTIVE_ERRORS = 10;
7
+ /**
8
+ * Device flow orchestration with every effect kept outside: the network behind the api port, time
9
+ * behind clock, cancellation behind signal. That is what makes expiry, slow_down, poll failures
10
+ * and Ctrl+C unit-testable.
11
+ */
12
+ export const runDeviceFlow = async (api, deps) => {
13
+ const { clock, signal } = deps;
14
+ const maxConsecutiveErrors = deps.maxConsecutiveErrors ?? DEFAULT_MAX_CONSECUTIVE_ERRORS;
15
+ throwIfAborted(signal);
16
+ const code = await api.requestDeviceCode();
17
+ deps.onCode(code);
18
+ const deadline = clock.now() + code.expiresInSec * 1000;
19
+ let intervalMs = Math.max(code.intervalSec * 1000, deps.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS);
20
+ let consecutiveErrors = 0;
21
+ for (;;) {
22
+ if (clock.now() >= deadline) {
23
+ throw new DeviceFlowExpiredError();
24
+ }
25
+ await clock.sleep(intervalMs, signal);
26
+ throwIfAborted(signal);
27
+ let result;
28
+ try {
29
+ result = await api.pollToken(code.deviceCode);
30
+ consecutiveErrors = 0;
31
+ }
32
+ catch (error) {
33
+ if (error instanceof CancelledError) {
34
+ throw error;
35
+ }
36
+ consecutiveErrors += 1;
37
+ if (consecutiveErrors >= maxConsecutiveErrors) {
38
+ throw error;
39
+ }
40
+ deps.onTransientError?.(error, consecutiveErrors);
41
+ continue;
42
+ }
43
+ switch (result.status) {
44
+ case 'authorized': {
45
+ return result.token;
46
+ }
47
+ case 'pending': {
48
+ break;
49
+ }
50
+ case 'slow_down': {
51
+ intervalMs += SLOW_DOWN_STEP_MS;
52
+ break;
53
+ }
54
+ case 'expired': {
55
+ throw new DeviceFlowExpiredError();
56
+ }
57
+ case 'denied': {
58
+ throw new DeviceFlowDeniedError();
59
+ }
60
+ }
61
+ }
62
+ };
@@ -0,0 +1,7 @@
1
+ /** Time as a dependency: device flow and retry take a Clock, so tests never wait for real. */
2
+ export type Clock = {
3
+ now(): number;
4
+ sleep(ms: number, signal?: AbortSignal): Promise<void>;
5
+ };
6
+ export declare const systemClock: Clock;
7
+ export declare const throwIfAborted: (signal: AbortSignal | undefined) => void;
@@ -0,0 +1,21 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ import { CancelledError, isAbortError } from './errors.js';
3
+ export const systemClock = {
4
+ now: () => Date.now(),
5
+ sleep: async (ms, signal) => {
6
+ try {
7
+ await delay(ms, undefined, signal ? { signal } : {});
8
+ }
9
+ catch (error) {
10
+ if (isAbortError(error)) {
11
+ throw new CancelledError();
12
+ }
13
+ throw error;
14
+ }
15
+ },
16
+ };
17
+ export const throwIfAborted = (signal) => {
18
+ if (signal?.aborted === true) {
19
+ throw new CancelledError();
20
+ }
21
+ };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Sdk errors carry no user-facing text and no exit code: the adapter assigns both
3
+ * (src/cli/errors.ts, and a future MCP server).
4
+ *
5
+ * Every error has a stable `kind`, and the adapter switches over it. An instanceof chain would
6
+ * let a new error class fall through to the fallback and become exit 1 with no readable text.
7
+ */
8
+ export type SdkErrorKind = 'api' | 'auth_required' | 'cancelled' | 'device_flow_denied' | 'device_flow_expired' | 'network' | 'storage' | 'validation';
9
+ /** Abstract on purpose: `new SdkError(...)` would be an error without a kind, unrecognisable. */
10
+ export declare abstract class SdkError extends Error {
11
+ abstract readonly kind: SdkErrorKind;
12
+ constructor(message: string, options?: ErrorOptions);
13
+ }
14
+ /** No response at all: DNS, refused connection, a dropped socket. */
15
+ export declare class NetworkError extends SdkError {
16
+ readonly kind = "network";
17
+ readonly url: string;
18
+ constructor(url: string, cause: unknown);
19
+ }
20
+ /** The server answered with an error. `code` and `details` follow the service's own format. */
21
+ export declare class ApiError extends SdkError {
22
+ readonly kind = "api";
23
+ readonly code: string | undefined;
24
+ readonly details: unknown;
25
+ readonly retryAfterMs: number | undefined;
26
+ readonly status: number;
27
+ constructor(init: {
28
+ code?: string | undefined;
29
+ details?: unknown;
30
+ message: string;
31
+ retryAfterMs?: number | undefined;
32
+ status: number;
33
+ });
34
+ }
35
+ /** missing: no token stored locally. rejected: the server answered 401. */
36
+ export type AuthRequiredReason = 'missing' | 'rejected';
37
+ export declare class AuthRequiredError extends SdkError {
38
+ readonly kind = "auth_required";
39
+ readonly reason: AuthRequiredReason;
40
+ constructor(reason: AuthRequiredReason);
41
+ }
42
+ export declare class CancelledError extends SdkError {
43
+ readonly kind = "cancelled";
44
+ constructor();
45
+ }
46
+ export type Issue = {
47
+ message: string;
48
+ /** The input field the problem belongs to. No path: the input as a whole is wrong. */
49
+ path?: string | undefined;
50
+ };
51
+ /** A business rule broken before any network call. */
52
+ export declare class ValidationError extends SdkError {
53
+ readonly kind = "validation";
54
+ readonly issues: readonly Issue[];
55
+ constructor(issues: readonly Issue[]);
56
+ }
57
+ export declare class DeviceFlowExpiredError extends SdkError {
58
+ readonly kind = "device_flow_expired";
59
+ constructor();
60
+ }
61
+ export declare class DeviceFlowDeniedError extends SdkError {
62
+ readonly kind = "device_flow_denied";
63
+ constructor();
64
+ }
65
+ /** The session file is there but unusable: corrupted JSON, or something else in its place. */
66
+ export declare class StorageError extends SdkError {
67
+ readonly kind = "storage";
68
+ readonly path: string;
69
+ constructor(message: string, path: string, options?: ErrorOptions);
70
+ }
71
+ /**
72
+ * Every concrete error: a switch over `kind` narrows to the class. instanceof on the abstract
73
+ * base gives the base, without .status, .url or .issues.
74
+ */
75
+ export type AnySdkError = ApiError | AuthRequiredError | CancelledError | DeviceFlowDeniedError | DeviceFlowExpiredError | NetworkError | StorageError | ValidationError;
76
+ export declare const isSdkError: (error: unknown) => error is AnySdkError;
77
+ type Assert<T extends true> = T;
78
+ /** Safety net: a kind whose class never made it into AnySdkError breaks the build here. */
79
+ export type AllKindsCovered = Assert<SdkErrorKind extends AnySdkError['kind'] ? true : false>;
80
+ export declare const isAbortError: (error: unknown) => boolean;
81
+ export {};