@sixtyninecommerce/admin 0.6.0 → 0.7.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/README.md +359 -2
- package/dist/apps/assertion.d.ts +30 -3
- package/dist/apps/assertion.js +40 -3
- package/dist/apps/config.d.ts +39 -0
- package/dist/apps/config.js +22 -0
- package/dist/apps/credential.d.ts +5 -0
- package/dist/apps/credential.js +55 -4
- package/dist/apps/index.d.ts +11 -0
- package/dist/client.d.ts +54 -1
- package/dist/client.js +34 -1
- package/dist/custom-objects/index.d.ts +66 -0
- package/dist/custom-objects/index.js +48 -0
- package/dist/customers/index.d.ts +225 -0
- package/dist/customers/index.js +279 -0
- package/dist/documents/index.d.ts +432 -0
- package/dist/documents/index.js +289 -0
- package/dist/index.d.ts +34 -2
- package/dist/index.js +16 -1
- package/dist/jobs/index.d.ts +143 -0
- package/dist/jobs/index.js +132 -0
- package/dist/metafields/index.d.ts +291 -0
- package/dist/metafields/index.js +141 -0
- package/dist/orders/index.d.ts +106 -0
- package/dist/orders/index.js +115 -0
- package/dist/orders/surcharges.d.ts +121 -0
- package/dist/orders/surcharges.js +110 -0
- package/dist/payments/index.d.ts +103 -0
- package/dist/payments/index.js +55 -0
- package/dist/payments/providers.d.ts +281 -0
- package/dist/payments/providers.js +205 -0
- package/dist/platform/provision-store.d.ts +9 -26
- package/dist/platform/provision-store.js +8 -66
- package/dist/tax/index.d.ts +128 -0
- package/dist/tax/index.js +101 -0
- package/dist/tenant/capabilities.d.ts +15 -0
- package/dist/types.d.ts +20 -3
- package/dist/uploads/index.d.ts +146 -0
- package/dist/uploads/index.js +72 -0
- package/package.json +3 -3
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { type Transport } from '@sixtyninecommerce/core';
|
|
2
|
+
import type { Call } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* What an upload is for. It decides which permission stages it and who may
|
|
5
|
+
* consume it.
|
|
6
|
+
*
|
|
7
|
+
* `asset` is the only purpose today. `bulk` and `document` arrive with the
|
|
8
|
+
* calls that consume them, each with its own storage decision — everything
|
|
9
|
+
* staged currently lands in the public asset bucket, which is right for a media
|
|
10
|
+
* asset and not for a customer export or an order attachment.
|
|
11
|
+
*/
|
|
12
|
+
export type UploadPurpose = 'asset';
|
|
13
|
+
export type UploadStatus = 'staged' | 'consumed' | 'expired';
|
|
14
|
+
export interface StagedUpload {
|
|
15
|
+
id: string;
|
|
16
|
+
purpose: UploadPurpose;
|
|
17
|
+
status: UploadStatus;
|
|
18
|
+
mimeType: string;
|
|
19
|
+
/**
|
|
20
|
+
* The EXACT size the signature was computed for, as a string.
|
|
21
|
+
*
|
|
22
|
+
* A string because it is a 64-bit column and a JSON number is a double; use
|
|
23
|
+
* `Number(...)` if you need arithmetic, which is exact below 2^53.
|
|
24
|
+
*/
|
|
25
|
+
sizeBytes: string;
|
|
26
|
+
/** The label you gave the file. It is never part of the object's location. */
|
|
27
|
+
filename: string | null;
|
|
28
|
+
/** When the RESERVATION lapses. After this nothing will accept the upload. */
|
|
29
|
+
expiresAt: string;
|
|
30
|
+
consumedAt: string | null;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
}
|
|
33
|
+
export interface StagedUploadTicket extends StagedUpload {
|
|
34
|
+
/** PUT the bytes here. Single use. */
|
|
35
|
+
url: string;
|
|
36
|
+
method: 'PUT';
|
|
37
|
+
/**
|
|
38
|
+
* Send ALL of these, spelled exactly like this. They are part of the
|
|
39
|
+
* signature, so storage refuses a PUT that omits or changes one.
|
|
40
|
+
*/
|
|
41
|
+
headers: Record<string, string>;
|
|
42
|
+
/**
|
|
43
|
+
* When the URL stops working — at most {@link StagedUpload.expiresAt}, and
|
|
44
|
+
* less than it when you re-staged an upload that was already running down.
|
|
45
|
+
*/
|
|
46
|
+
urlExpiresAt: string;
|
|
47
|
+
}
|
|
48
|
+
export interface StageUploadInput {
|
|
49
|
+
purpose: UploadPurpose;
|
|
50
|
+
/** Checked against the platform allow-list and bound into the signature. */
|
|
51
|
+
mimeType: string;
|
|
52
|
+
/**
|
|
53
|
+
* The EXACT number of bytes you will send — not a maximum.
|
|
54
|
+
*
|
|
55
|
+
* A presigned PUT can only pin a precise `Content-Length`; there is no way to
|
|
56
|
+
* sign an upper bound. Storage refuses any other length, so this must be the
|
|
57
|
+
* real size of the body (`blob.size`, `buffer.length`, `stat.size`).
|
|
58
|
+
*/
|
|
59
|
+
size: number;
|
|
60
|
+
/**
|
|
61
|
+
* Your own handle for this upload, making the call repeatable.
|
|
62
|
+
*
|
|
63
|
+
* Required. Staging is the call that hands out a URL, and a dropped response
|
|
64
|
+
* on it is the ordinary failure: without a token every retry would reserve
|
|
65
|
+
* another place and leave an object nobody ever claims. Staging twice with
|
|
66
|
+
* the same token returns the SAME upload; staging DIFFERENT terms under it is
|
|
67
|
+
* refused with `UPLOAD_TOKEN_CONFLICT`.
|
|
68
|
+
*/
|
|
69
|
+
clientToken: string;
|
|
70
|
+
/** A label for humans. It never reaches the object's location. */
|
|
71
|
+
filename?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Bytes this SDK knows how to PUT. A `Blob` in any modern runtime, or a
|
|
75
|
+
* `Uint8Array`/`ArrayBuffer` from Node.
|
|
76
|
+
*/
|
|
77
|
+
export type UploadBody = Blob | ArrayBuffer | ArrayBufferView;
|
|
78
|
+
export interface ClaimAsAssetInput {
|
|
79
|
+
/** Place the asset in this CMS asset folder. */
|
|
80
|
+
folderId?: string;
|
|
81
|
+
altText?: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Signed direct-to-storage uploads (spec §6.11).
|
|
85
|
+
*
|
|
86
|
+
* **The shape of it.** You say what you are uploading; the platform decides
|
|
87
|
+
* WHERE it goes and signs exactly that. You never see or choose a path. The
|
|
88
|
+
* three steps are `stage` (reserve and get a URL), `put` (send the bytes
|
|
89
|
+
* straight to storage — they do not pass through the platform), and a consuming
|
|
90
|
+
* call that takes the upload's id.
|
|
91
|
+
*
|
|
92
|
+
* ```ts
|
|
93
|
+
* const bytes = await readFile('hero.png');
|
|
94
|
+
* const ticket = await admin.uploads.stage({
|
|
95
|
+
* purpose: 'asset',
|
|
96
|
+
* mimeType: 'image/png',
|
|
97
|
+
* size: bytes.length,
|
|
98
|
+
* clientToken: crypto.randomUUID(),
|
|
99
|
+
* filename: 'hero.png',
|
|
100
|
+
* });
|
|
101
|
+
* await admin.uploads.put(ticket, bytes);
|
|
102
|
+
* const asset = await admin.uploads.claimAsAsset(ticket.id, { altText: 'Our hero' });
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* **The size is exact.** `size` is the byte count storage will accept and no
|
|
106
|
+
* other — not a ceiling. Sending a different number of bytes fails at storage
|
|
107
|
+
* with a signature error, so compute it from the body you are about to send.
|
|
108
|
+
*
|
|
109
|
+
* **One use, and a window scaled to the size.** A URL writes one object, once.
|
|
110
|
+
* The reservation lasts at least fifteen minutes, and longer for a large file —
|
|
111
|
+
* enough to send it at about 1 Mbit/s — because a window too short to upload in
|
|
112
|
+
* would expire between the PUT and the claim. `expiresAt` is the reservation;
|
|
113
|
+
* `urlExpiresAt` is the signature, and never outlives it. An upload nobody
|
|
114
|
+
* consumes expires and its bytes are deleted.
|
|
115
|
+
*
|
|
116
|
+
* **It is yours alone.** An upload is visible and usable only to the principal
|
|
117
|
+
* that staged it, on the store it was staged for. Anything else answers
|
|
118
|
+
* `UPLOAD_NOT_FOUND` — the same answer as an id that never existed.
|
|
119
|
+
*
|
|
120
|
+
* **Browsers need CORS.** `put` sends a cross-origin PUT to the object store,
|
|
121
|
+
* which requires CORS rules on the bucket that this platform does not
|
|
122
|
+
* configure. Server-side callers are unaffected.
|
|
123
|
+
*/
|
|
124
|
+
export interface UploadsModule {
|
|
125
|
+
/** Reserve a place and get a signed URL to PUT one file to. */
|
|
126
|
+
stage(input: StageUploadInput, options?: Call): Promise<StagedUploadTicket>;
|
|
127
|
+
/**
|
|
128
|
+
* Send the bytes to storage using the ticket's URL and headers.
|
|
129
|
+
*
|
|
130
|
+
* It does NOT go through the platform and carries none of your credentials —
|
|
131
|
+
* the URL is the authorisation. Resolves on a 2xx and throws the store's
|
|
132
|
+
* status otherwise.
|
|
133
|
+
*/
|
|
134
|
+
put(ticket: StagedUploadTicket, body: UploadBody): Promise<void>;
|
|
135
|
+
/** One upload's state. Unknown, or another principal's, is `UPLOAD_NOT_FOUND`. */
|
|
136
|
+
get(id: string, options?: Call): Promise<StagedUpload>;
|
|
137
|
+
/**
|
|
138
|
+
* Turn a staged upload into a CMS asset.
|
|
139
|
+
*
|
|
140
|
+
* This is the consuming call: it verifies the object is there and is what was
|
|
141
|
+
* signed, spends the upload, and ingests the bytes. An upload can be consumed
|
|
142
|
+
* once — a second call is `UPLOAD_ALREADY_CONSUMED`.
|
|
143
|
+
*/
|
|
144
|
+
claimAsAsset(id: string, input?: ClaimAsAssetInput, options?: Call): Promise<unknown>;
|
|
145
|
+
}
|
|
146
|
+
export declare function createUploadsModule(transport: Transport, fetchImpl: typeof globalThis.fetch): UploadsModule;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { PLATFORM_PASSTHROUGH_KEYS, ValidationError, } from '@sixtyninecommerce/core';
|
|
2
|
+
/**
|
|
3
|
+
* `headers` carries the exact header NAMES a signature was computed over.
|
|
4
|
+
*
|
|
5
|
+
* Case-converting them would rewrite `content-type` to `contentType`, and the
|
|
6
|
+
* PUT would then go out without the header storage is checking for — a
|
|
7
|
+
* signature failure that looks like a credential problem and is not.
|
|
8
|
+
*/
|
|
9
|
+
const PRESERVE = new Set([...PLATFORM_PASSTHROUGH_KEYS, 'headers']);
|
|
10
|
+
export function createUploadsModule(transport, fetchImpl) {
|
|
11
|
+
return {
|
|
12
|
+
async stage(input, options) {
|
|
13
|
+
if (!Number.isSafeInteger(input.size) || input.size < 1) {
|
|
14
|
+
// Refused here rather than sent: a fractional or negative size would be
|
|
15
|
+
// rejected by the service as a validation failure whose message is
|
|
16
|
+
// about a field the caller did not type, and `size: NaN` from a missing
|
|
17
|
+
// `blob.size` is the way this goes wrong in practice.
|
|
18
|
+
throw new ValidationError({
|
|
19
|
+
code: 'UPLOAD_SIZE_INVALID',
|
|
20
|
+
message: `size must be the exact whole number of bytes you will upload; received ${String(input.size)}.`,
|
|
21
|
+
status: 400,
|
|
22
|
+
service: 'plugins',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return await transport.rest('plugins', {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
path: '/uploads/stage',
|
|
28
|
+
body: {
|
|
29
|
+
purpose: input.purpose,
|
|
30
|
+
mime_type: input.mimeType,
|
|
31
|
+
size_bytes: input.size,
|
|
32
|
+
client_token: input.clientToken,
|
|
33
|
+
...(input.filename === undefined ? {} : { filename: input.filename }),
|
|
34
|
+
},
|
|
35
|
+
}, { ...options, preserveKeys: PRESERVE });
|
|
36
|
+
},
|
|
37
|
+
async put(ticket, body) {
|
|
38
|
+
const response = await fetchImpl(ticket.url, {
|
|
39
|
+
method: ticket.method,
|
|
40
|
+
// Exactly what was signed, and nothing else. An extra header is
|
|
41
|
+
// harmless, but a MISSING or altered one is a signature failure.
|
|
42
|
+
headers: { ...ticket.headers },
|
|
43
|
+
body: body,
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
const detail = await response.text().catch(() => '');
|
|
47
|
+
throw new ValidationError({
|
|
48
|
+
code: 'UPLOAD_PUT_REFUSED',
|
|
49
|
+
message: `The object store refused the upload (HTTP ${String(response.status)}). The commonest causes are a body whose length is not the \`size\` this upload was staged for, ` +
|
|
50
|
+
`a changed \`content-type\`, and a URL that has expired.${detail ? ` Store said: ${detail.slice(0, 300)}` : ''}`,
|
|
51
|
+
status: response.status,
|
|
52
|
+
service: 'plugins',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
async get(id, options) {
|
|
57
|
+
return await transport.rest('plugins', { method: 'GET', path: `/uploads/${encodeURIComponent(id)}` }, { ...options, preserveKeys: PRESERVE });
|
|
58
|
+
},
|
|
59
|
+
async claimAsAsset(id, input, options) {
|
|
60
|
+
const body = await transport.rest('cms', {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
path: '/assets/from-staged',
|
|
63
|
+
body: {
|
|
64
|
+
staged_upload_id: id,
|
|
65
|
+
...(input?.folderId === undefined ? {} : { folder_id: input.folderId }),
|
|
66
|
+
...(input?.altText === undefined ? {} : { alt_text: input.altText }),
|
|
67
|
+
},
|
|
68
|
+
}, options);
|
|
69
|
+
return body.data;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sixtyninecommerce/admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Back-office SDK for sixtynine-commerce: every capability of the platform as a method",
|
|
5
5
|
"homepage": "https://github.com/SIXTYNINE-digital/sixtynine-commerce-engine/tree/main/packages/admin-client#readme",
|
|
6
6
|
"repository": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@sixtyninecommerce/core": "^0.
|
|
36
|
+
"@sixtyninecommerce/core": "^0.4.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/node": "22.19.7",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"vitest": "4.1.6"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
|
-
"build": "tsc -p tsconfig.build.json",
|
|
44
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
45
45
|
"dev": "tsc -p tsconfig.build.json --watch",
|
|
46
46
|
"typecheck": "tsc --noEmit",
|
|
47
47
|
"test": "vitest run",
|