@hyperscale0/sdk 1.0.338 → 1.0.340

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/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ `@hyperscale0/sdk` ships as `1.0.N` with Hyperscale release rN, so `1.0.341` is the client for r341. Install `@latest` or pin `^1.0.0`. The `3.x` and `4.x` versions on npm are an old numbering, deprecated, and older than every `1.0.N`.
4
+
5
+ Newest first. A release that changed nothing the SDK sees has no entry.
6
+
7
+ ## 1.0.341
8
+
9
+ ### Added
10
+
11
+ - `npx @hyperscale0/sdk types --product <prd_…> --name <Name>` prints a declaration of your Product's kinds, actions, fields and inputs. `createClient<Name>()` then rejects a kind or action the Product does not have, at compile time.
12
+ - `product.books.retrieve` reads a Product's position, revenue to date and monthly books in one call.
13
+ - `product.objects.execute` answers with `receiptId`, the same id as the `hyperscale-receipt-id` header.
14
+ - This changelog ships in the package.
15
+
16
+ ### Changed
17
+
18
+ - A retrieve answers with the resource itself. `customer.retrieve`, `account.retrieve`, `receipt.retrieve`, `operation.retrieve`, `internal_transfer.retrieve`, `webhook.endpoint.retrieve` and the other resource retrieves used to answer `{ item }`; read the fields off the result.
19
+ - Prices read in halalas like every other amount. The fees and rates on `product.retrieve`, `product.activation.retrieve`, `composer.plan`, `catalog.pricing.retrieve`, `product.margin.retrieve` and each `usage.statement` rate used to be riyal decimals: a `monthly` of `"2500"` now reads `"250000"`, and a rate of `"0.25"` reads `"25"`. `expectedPricing` takes halalas.
20
+ - The client sends `Accept: application/json`, so `account.statement.export` keeps answering with the JSON envelope while curl gets the CSV file.
21
+
22
+ ## 1.0.340
23
+
24
+ ### Removed
25
+
26
+ - The route table no longer lists `product.list`, `product.retrieve`, `developer.log.list`, `developer.log.retrieve`, `transfer_return.create` or `usage.statement`. A Product key could not call them. `call` with one of these names now throws `Unknown operation` instead of sending a request the server refuses.
27
+
28
+ ## 1.0.339
29
+
30
+ ### Added
31
+
32
+ - `HyperscaleError` carries the server's `code`, `message`, `details`, `requestId`, `nextStep` and `shortfall`. `HyperscaleConnectionError` means no readable answer arrived.
33
+ - The client makes an idempotency key for every write that needs one and keeps it across its own retries.
34
+ - `objects.run` reads an action, throws its blocker, and sends the revision, Build and target it read.
35
+ - `paginate` walks every list. `productId` is optional in inputs.
package/README.md CHANGED
@@ -1,85 +1,122 @@
1
1
  # @hyperscale0/sdk
2
2
 
3
- Read [how Hyperscale fits](https://hyperscale0.ai/docs/runtime.md#how-hyperscale-fits). The SDK calls the shared operation API and grants no additional authority.
3
+ TypeScript client for running a Hyperscale Product: customers, payments, schedules and the sandbox clock.
4
4
 
5
- One client for every Hyperscale Product. Platform types ship with the package.
6
- Object kinds, fields and action availability come from the admitted Build at runtime.
5
+ ```sh
6
+ npm i @hyperscale0/sdk
7
+ ```
7
8
 
8
- ```js
9
- import { createClient } from "@hyperscale0/sdk";
9
+ The SDK runs a Product that already exists. Create the Product with `hyperscale product create` or in the Build portal, and copy its `prd_…` id. `hyperscale key create --product <id>` shows the key's secret once. Export them as `HYPERSCALE_PRODUCT_ID` and `HYPERSCALE_API_KEY`.
10
+
11
+ ## Quickstart
12
+
13
+ This runs as written in sandbox against the gym program from the [HSX guide](https://hyperscale0.ai/docs/hsx.md#schedules): a `membership` object with `start_membership`, `accept_membership` and `end_membership`.
14
+
15
+ ```ts
16
+ import { createClient, HyperscaleError } from "@hyperscale0/sdk";
10
17
 
11
- const productId = process.env.HYPERSCALE_PRODUCT_ID;
12
- const client = createClient({
18
+ const hs = createClient({
13
19
  apiKey: process.env.HYPERSCALE_API_KEY,
14
- productId,
20
+ productId: process.env.HYPERSCALE_PRODUCT_ID,
15
21
  });
16
- const discovery = await client.discover();
17
- // Choose a kind and supply metadata according to its createSchema.
18
- const kind = discovery.kinds[0].kind;
19
- const object = await client.call(
20
- "product.objects.create",
21
- { productId, kind },
22
- {
23
- idempotencyKey: "object-create-001",
24
- },
25
- );
26
- const available = await client.call("product.objects.actions", {
27
- productId,
28
- kind,
29
- objectId: object.objectId,
22
+
23
+ // A test customer with SAR 300 in a sandbox balance. Amounts are halalas.
24
+ const { customerId } = await hs.call("customer.create", {
25
+ entity: { kind: "person", displayName: "Noura Alharbi", country: "SA" },
26
+ test: true,
27
+ });
28
+ const { accountId } = await hs.call("account.create", {
29
+ owner: { type: "customer", id: customerId },
30
+ role: "customer_balance",
31
+ currency: "SAR",
32
+ });
33
+ await hs.call("sandbox.account.fund", {
34
+ destinationAccountId: accountId,
35
+ amount: "30000",
36
+ currency: "SAR",
30
37
  });
31
- ```
32
38
 
33
- Choose the intended action by name and check `availability.status`. An
34
- `unavailable` action lists its `blockers`. A `requires_input` action is runnable
35
- once you supply what it names in `subjectFields` and `actionInputs`: collect its
36
- `requiredNow` fields and requirements against `fieldsSchema` and `inputSchema`.
37
- After that review, execute it with the complete context returned by the server:
39
+ // A membership Noura owns. The gym starts it tomorrow and she accepts it.
40
+ const { effectiveAt } = await hs.call("sandbox.clock.retrieve", {});
41
+ const day = (n) =>
42
+ new Date(Date.parse(effectiveAt) + n * 86_400_000).toISOString();
43
+ const { objectId } = await hs.call("product.objects.create", {
44
+ kind: "membership",
45
+ fields: { plan: "Monthly" },
46
+ onBehalfOf: customerId,
47
+ });
48
+ await hs.objects.run("membership", objectId, "start_membership", {
49
+ inputs: { startsAt: day(1) },
50
+ });
51
+ await hs.objects.run("membership", objectId, "accept_membership", {
52
+ onBehalfOf: customerId,
53
+ });
38
54
 
39
- ```js
40
- const action = available.actions.find((item) => item.name === chosenActionName);
41
- if (!action || action.availability.status === "unavailable") {
42
- throw new Error("The selected action is unavailable");
43
- }
44
- const result = await client.call(
45
- "product.objects.execute",
46
- {
47
- productId,
48
- kind,
49
- objectId: object.objectId,
50
- action: action.name,
51
- expectedRevision: available.revision,
52
- productBuildId: action.productBuildId,
53
- digest: action.digest,
54
- target: action.target,
55
- fields: collectedFields,
56
- input: collectedInput,
57
- },
58
- { idempotencyKey: "object-action-001" },
55
+ // Two days on, the first SAR 250 of dues runs.
56
+ const { ran } = await hs.call("sandbox.clock.advance", { at: day(2) });
57
+ console.log(
58
+ ran.map((piece) => `${piece.action} ${piece.amount?.value} ${piece.status}`),
59
59
  );
60
+
61
+ // A refusal carries the server's own words.
62
+ try {
63
+ await hs.objects.run("membership", objectId, "end_membership", {
64
+ onBehalfOf: customerId,
65
+ });
66
+ } catch (error) {
67
+ if (!(error instanceof HyperscaleError)) throw error;
68
+ console.log(error.code, error.message);
69
+ }
60
70
  ```
61
71
 
62
- `chosenActionName`, `collectedFields` and `collectedInput` are application inputs
63
- from that review. Object creation does not create an agreement or move money.
64
- Use `product.objects.list` and `product.objects.retrieve` to read stored objects.
65
- `discover()` returns the authored object contract in one request, including fields
66
- and exposed actions. All six operations also use the descriptor-backed generic
67
- operation transport in `client.call`. Names and types match the operation descriptors.
68
-
69
- To take a customer's step, such as a buyer paying into escrow, add
70
- `onBehalfOf: "<customer id>"` to `product.objects.create`, `actions`, `execute`
71
- or `preview`. The call runs with that customer's accounts, the Product key keeps
72
- its view of every record, and the agreement records both the key and the
73
- customer. A create with `onBehalfOf` makes that customer the owner.
74
-
75
- Copy the whole target, including `instanceId` when present. Refresh availability
76
- after a revision or Build conflict. Reuse the same key and body for an uncertain
77
- request. The client never retries a mutation automatically.
78
-
79
- Low-level `instruments()`, `actions()` and `act()` remain for admitted inspection
80
- and operations. Instrument creation stays internal to object attachment execution.
81
- Keys stay in your environment. Set `baseUrl` and `environment` to select an estate
82
- and sandbox or live plane.
72
+ `objects.run` reads the action first. If it can't run, it throws `HyperscaleError` with the reason. Otherwise it sends the revision, Build and target it read.
73
+
74
+ ## Types
75
+
76
+ Until you generate your Product's types, `kind`, `action`, `fields` and `input` take any string and any record. The generator reads `product.objects.discover` with `HYPERSCALE_API_KEY`, plus `HYPERSCALE_BASE_URL` and `HYPERSCALE_ENVIRONMENT` when set, and prints one declaration:
77
+
78
+ ```sh
79
+ npx @hyperscale0/sdk types --product $HYPERSCALE_PRODUCT_ID --name DeskAndKey > desk-and-key.d.ts
80
+ ```
81
+
82
+ ```ts
83
+ import { createClient } from "@hyperscale0/sdk";
84
+ import type { DeskAndKey } from "./desk-and-key";
85
+
86
+ const hs = createClient<DeskAndKey>({
87
+ apiKey: process.env.HYPERSCALE_API_KEY,
88
+ productId: process.env.HYPERSCALE_PRODUCT_ID,
89
+ });
90
+ ```
91
+
92
+ With the type argument, a kind such as `"day-pass"` when the Product has `day_pass`, or an action such as `"pay_daypass"`, fails to compile instead of failing at runtime. `objects.run`, `product.objects.create`, `product.objects.execute`, `product.objects.preview` and every call that takes a `kind` check against it, and money fields are minor-unit strings. Regenerate the file after each publish. Without a type argument, `createClient()` takes any kind and action.
93
+
94
+ ## Errors and retries
95
+
96
+ Every refusal is a `HyperscaleError` with the server's `message`, `code`, `status`, `details` and `requestId`, plus `nextStep` and `shortfall` when the server sends them. When no readable answer arrives (a timeout, a dropped connection, an HTML error page), the client throws `HyperscaleConnectionError`: the call may have run, so retry it with the same `idempotencyKey`.
97
+
98
+ The client makes an idempotency key for every write that needs one and keeps it across its own retries. Pass `{ idempotencyKey }` as the third argument to choose it. Reads retry on 408, 429 and 5xx, and writes retry only under a key. Set `timeout` (default 30000 ms) and `maxRetries` (default 2) in `createClient`.
99
+
100
+ ## Operations
101
+
102
+ `client.call(name, input)` runs any operation, and names autocomplete. `productId` comes from the client. `client.paginate(name, input)` iterates every record of a list.
103
+
104
+ | Job | Operations |
105
+ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
106
+ | Objects | `product.objects.discover`, `product.objects.create`, `product.objects.list`, `product.objects.retrieve`, `product.objects.actions`, `product.objects.execute`, `product.objects.timeline.list` |
107
+ | Customers | `customer.create`, `customer.list`, `customer.retrieve` |
108
+ | Money | `account.create`, `account.balance.retrieve`, `account.statement.retrieve`, `internal_transfer.list`, `receipt.list` |
109
+ | Sandbox | `sandbox.account.fund`, `sandbox.clock.retrieve`, `sandbox.clock.advance` |
110
+ | The business | `activity.list`, `product.schedule.list`, `product.work.list`, `product.treasury.retrieve`, `product.books.retrieve` |
111
+ | Webhooks | `webhook.endpoint.create`, `webhook.endpoint.list`, `webhook.delivery.list`, `event.list` |
112
+
113
+ Your Product's own operations from `key create`, such as `membership_dues_instances_list`, go through `call` too. [openapi.json](https://hyperscale0.ai/openapi.json) lists every platform operation with its schema, and [Product actions](https://hyperscale0.ai/docs/runtime.md) explains how they fit together.
114
+
115
+ Set `environment: "live"` for live money; the default is sandbox.
116
+
117
+ ## Versions
118
+
119
+ The SDK ships as `1.0.N` with Hyperscale release rN. `CHANGELOG.md`, in the package, lists what each version added, changed and removed. The `3.x` and `4.x` versions on npm are deprecated and older than every `1.0.N`, so pin `^1.0.0`.
83
120
 
84
121
  Security: https://hyperscale0.ai/security
85
122
 
package/bin.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { typesCommand } from "./product-types.js";
3
+
4
+ try {
5
+ process.stdout.write(await typesCommand(process.argv.slice(2), process.env));
6
+ } catch (error) {
7
+ process.stderr.write(
8
+ `${error.code ? `${error.code}: ` : ""}${error.message}\n`,
9
+ );
10
+ process.exitCode = 1;
11
+ }
package/client.d.ts CHANGED
@@ -1,46 +1,193 @@
1
1
  import type {
2
+ ErrorCode,
3
+ ListOperations,
2
4
  Operations,
3
- payment_reminder_delivery_retrieveOutput,
5
+ product_objects_actionsOutput,
4
6
  product_objects_discoverOutput,
5
- product_instruments_listOutput,
6
- product_actions_listOutput,
7
+ product_objects_executeOutput,
7
8
  } from "./platform.js";
8
9
  export type * from "./platform.js";
9
10
 
10
11
  export interface ClientOptions {
12
+ /** A Product key from `hyperscale key create`. */
11
13
  apiKey?: string;
14
+ /** A customer session token, in place of a Product key. */
12
15
  sessionToken?: string;
16
+ /** The key's Product. Calls fill it in when their input leaves it out. */
13
17
  productId: string;
18
+ /** Defaults to https://hyperscale0.ai. */
14
19
  baseUrl?: string;
20
+ /** Defaults to sandbox. */
15
21
  environment?: "sandbox" | "live";
22
+ /** Milliseconds each attempt may take. Defaults to 30000. */
23
+ timeout?: number;
24
+ /** Retries for reads, and for writes under an idempotency key. Defaults to 2. */
25
+ maxRetries?: number;
16
26
  turnstileToken?: string;
17
27
  }
18
- export interface ActionInput {
28
+
29
+ export interface CallOptions {
30
+ /** The client makes one for operations that need it; yours wins. */
31
+ idempotencyKey?: string;
32
+ turnstileToken?: string;
33
+ headers?: Record<string, string>;
34
+ }
35
+
36
+ /** One action of a generated Product type: subject fields and input. */
37
+ export interface ActionTypes {
38
+ fields: Record<string, unknown>;
39
+ input: Record<string, unknown>;
40
+ }
41
+
42
+ /**
43
+ * What `npx @hyperscale0/sdk types --product <id>` generates. Pass it as
44
+ * `createClient<P>()` to type each kind, its fields, its actions and their
45
+ * input. Without it, kinds and actions are any string.
46
+ */
47
+ export interface ProductTypes {
48
+ kinds: Record<
49
+ string,
50
+ { fields: Record<string, unknown>; actions: Record<string, ActionTypes> }
51
+ >;
52
+ }
53
+
54
+ type KindName<P extends ProductTypes> = keyof P["kinds"] & string;
55
+ type ActionName<
56
+ P extends ProductTypes,
57
+ Kind extends KindName<P>,
58
+ > = keyof P["kinds"][Kind]["actions"] & string;
59
+
60
+ export interface RunOptions<Action extends ActionTypes = ActionTypes> {
61
+ /** The customer the action runs for, such as the object's owner. */
62
+ onBehalfOf?: string;
63
+ fields?: Action["fields"];
64
+ inputs?: Action["input"];
65
+ /** Chooses one agreement when the action runs on several. */
19
66
  instanceId?: string;
20
- fields?: Record<string, unknown>;
21
- parties?: Record<string, string>;
22
- input?: Record<string, unknown>;
23
- }
24
- export interface Client {
25
- reminderDelivery(
26
- operationId: string,
27
- ): Promise<payment_reminder_delivery_retrieveOutput>;
67
+ idempotencyKey?: string;
68
+ }
69
+
70
+ /** Input with productId optional, since the client fills it in. */
71
+ export type CallInput<T> = T extends { readonly productId: string }
72
+ ? { [K in keyof T as K extends "productId" ? never : K]: T[K] } & {
73
+ readonly productId?: string;
74
+ }
75
+ : T;
76
+
77
+ type Narrow<T, U> = Omit<T, keyof U> & U;
78
+
79
+ /**
80
+ * Call input with kind, action, fields and input narrowed to a generated
81
+ * Product type. Without one, it is CallInput unchanged.
82
+ */
83
+ export type ProductCallInput<
84
+ P extends ProductTypes,
85
+ Name extends keyof Operations,
86
+ > = string extends keyof P["kinds"]
87
+ ? CallInput<Operations[Name]["input"]>
88
+ : Name extends "product.objects.create"
89
+ ? {
90
+ [Kind in KindName<P>]: Narrow<
91
+ CallInput<Operations[Name]["input"]>,
92
+ { readonly kind: Kind; readonly fields?: P["kinds"][Kind]["fields"] }
93
+ >;
94
+ }[KindName<P>]
95
+ : Name extends "product.objects.execute" | "product.objects.preview"
96
+ ? {
97
+ [Kind in KindName<P>]: {
98
+ [Action in ActionName<P, Kind>]: Narrow<
99
+ CallInput<Operations[Name]["input"]>,
100
+ {
101
+ readonly kind: Kind;
102
+ readonly action: Action;
103
+ readonly fields?: P["kinds"][Kind]["actions"][Action]["fields"];
104
+ readonly input?: P["kinds"][Kind]["actions"][Action]["input"];
105
+ }
106
+ >;
107
+ }[ActionName<P, Kind>];
108
+ }[KindName<P>]
109
+ : Operations[Name]["input"] extends { readonly kind: infer Kind }
110
+ ? string extends Kind
111
+ ? Narrow<
112
+ CallInput<Operations[Name]["input"]>,
113
+ { readonly kind: KindName<P> }
114
+ >
115
+ : CallInput<Operations[Name]["input"]>
116
+ : CallInput<Operations[Name]["input"]>;
117
+
118
+ type Unavailable = Extract<
119
+ product_objects_actionsOutput["actions"][number]["availability"],
120
+ { status: "unavailable" }
121
+ >;
122
+ export type ActionBlockerCode = Unavailable["blockers"][number]["code"];
123
+
124
+ export type ListItem<Name extends keyof ListOperations & keyof Operations> =
125
+ Operations[Name]["output"] extends {
126
+ readonly [Key in ListOperations[Name]]: ReadonlyArray<infer Item>;
127
+ }
128
+ ? Item
129
+ : never;
130
+
131
+ /** A refusal from Hyperscale: `message` is the server's own text. */
132
+ export declare class HyperscaleError extends Error {
133
+ readonly code: ErrorCode | ActionBlockerCode | "connection_error";
134
+ /** The HTTP status, when an answer arrived. */
135
+ readonly status?: number;
136
+ readonly details?: Readonly<Record<string, unknown>>;
137
+ readonly requestId?: string;
138
+ /** What to do next, when the server names it. */
139
+ readonly nextStep?: string;
140
+ /** Minor units missing from the paying account, when money ran short. */
141
+ readonly shortfall?: string;
142
+ }
143
+
144
+ /** No readable answer: the request may have run. Retry with the same key. */
145
+ export declare class HyperscaleConnectionError extends HyperscaleError {
146
+ readonly code: "connection_error";
147
+ /** The key the request carried; pass it again to retry safely. */
148
+ readonly idempotencyKey?: string;
149
+ }
150
+
151
+ export declare function isHyperscaleError(
152
+ error: unknown,
153
+ ): error is HyperscaleError;
154
+
155
+ export interface Client<P extends ProductTypes = ProductTypes> {
156
+ /** The Product's object kinds, their fields and their actions. */
28
157
  discover(): Promise<product_objects_discoverOutput>;
29
158
  call<Name extends keyof Operations>(
30
159
  name: Name,
31
- input: Operations[Name]["input"],
32
- options?: {
33
- idempotencyKey?: string;
34
- turnstileToken?: string;
35
- headers?: Record<string, string>;
36
- },
160
+ input: ProductCallInput<P, Name>,
161
+ options?: CallOptions,
37
162
  ): Promise<Operations[Name]["output"]>;
38
- instruments(): Promise<product_instruments_listOutput>;
39
- actions(instrument?: string): Promise<product_actions_listOutput>;
40
- act(
41
- name: string,
42
- input: ActionInput,
43
- options: { idempotencyKey: string },
44
- ): Promise<Record<string, unknown>>;
45
- }
46
- export function createClient(options: ClientOptions): Client;
163
+ /** An operation of your own Product, by id or name from `key create`. */
164
+ call<Name extends string>(
165
+ // A typed client sends platform operations through the overload above only.
166
+ name: string extends keyof P["kinds"]
167
+ ? Name
168
+ : Name extends keyof Operations
169
+ ? never
170
+ : Name,
171
+ input?: Record<string, unknown>,
172
+ options?: CallOptions,
173
+ ): Promise<unknown>;
174
+ /** Every record of a cursor list, page by page. */
175
+ paginate<Name extends keyof ListOperations & keyof Operations>(
176
+ name: Name,
177
+ input: ProductCallInput<P, Name>,
178
+ options?: CallOptions,
179
+ ): AsyncGenerator<ListItem<Name>, void, undefined>;
180
+ objects: {
181
+ /** Reads the action's current state, then runs it. */
182
+ run<Kind extends KindName<P>, Action extends ActionName<P, Kind>>(
183
+ kind: Kind,
184
+ objectId: string,
185
+ action: Action,
186
+ options?: RunOptions<P["kinds"][Kind]["actions"][Action]>,
187
+ ): Promise<product_objects_executeOutput>;
188
+ };
189
+ }
190
+ /** Pass a type from `npx @hyperscale0/sdk types` as P to type kinds and actions. */
191
+ export function createClient<P extends ProductTypes = ProductTypes>(
192
+ options: ClientOptions,
193
+ ): Client<P>;