@sixtyninecommerce/admin 0.3.1 → 0.5.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 +114 -1
- package/dist/apps/assertion.d.ts +72 -0
- package/dist/apps/assertion.js +141 -0
- package/dist/apps/credential.d.ts +36 -0
- package/dist/apps/credential.js +182 -0
- package/dist/apps/index.d.ts +106 -0
- package/dist/apps/index.js +92 -0
- package/dist/client.d.ts +28 -3
- package/dist/client.js +118 -7
- package/dist/heads/index.d.ts +103 -0
- package/dist/heads/index.js +74 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +1 -0
- package/dist/platform/apps.d.ts +76 -0
- package/dist/platform/apps.js +153 -0
- package/dist/platform/provision-store.d.ts +157 -0
- package/dist/platform/provision-store.js +140 -0
- package/dist/types.d.ts +36 -10
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -25,15 +25,22 @@ export type { DeprecationNotice, Page, PageMeta, RateLimitInfo, RetryPolicy, } f
|
|
|
25
25
|
*/
|
|
26
26
|
export { AuthenticationError, BusinessRuleError, ConflictError, type ErrorDetail, NotFoundError, PermissionDeniedError, PlatformError, type ProblemDetails, RateLimitedError, TransportError, UpstreamError, ValidationError, } from '@sixtyninecommerce/core';
|
|
27
27
|
export type { ApiTokensModule, CreatedApiToken } from './api-tokens/index.js';
|
|
28
|
+
export type { AppCryptoKey, AppPrivateKey } from './apps/assertion.js';
|
|
29
|
+
export type { AppCredential, AppCredentialConfig } from './apps/credential.js';
|
|
30
|
+
export type { App, AppInstallation, ApproveConsentInput, AppsModule, InstallAppInput, } from './apps/index.js';
|
|
28
31
|
export type { AuditCall, AuditCallFilter, AuditCorrelation, AuditEvent, AuditEventFilter, AuditModule, } from './audit/index.js';
|
|
29
32
|
export type { AuthModule } from './auth/index.js';
|
|
30
|
-
export { type AdminClient, type AdminClientConfig, createAdminClient, type SessionConfig, } from './client.js';
|
|
33
|
+
export { type AdminClient, type AdminClientConfig, type AppAuthConfig, createAdminClient, type SessionConfig, } from './client.js';
|
|
31
34
|
export type { Form, FormField, FormFieldInput, FormFieldTranslation, FormFieldTranslationInput, FormInput, FormsModule, FormTranslation, FormTranslationInput, } from './content/forms.js';
|
|
32
35
|
export { AdminProjectionError } from './errors.js';
|
|
36
|
+
export type { CreateHeadInput, Head, HeadsModule, UpdateHeadInput, } from './heads/index.js';
|
|
33
37
|
export type { LocationProfile, LocationStockLocation, LocationsModule, OpeningHours, OpeningHoursDay, OpeningHoursInterval, StockLocationAddress, StockLocationAddressInput, UpsertLocationProfileInput, } from './locations/index.js';
|
|
34
38
|
export { OPENING_HOURS_DAYS } from './locations/index.js';
|
|
35
39
|
export type { ApiTokenTemplate, JsonSchema, LabelMap, MetaCatalog, MetaCatalogLabels, MetaCatalogPermissions, MetaCatalogPlugins, MetaCatalogTransitions, MetaModule, PermissionGroup, PluginCatalogEntry, RoleTemplate, StatusMetaMap, StatusTone, TransitionMap, } from './meta/index.js';
|
|
40
|
+
export type { AppKey, PlatformAppsModule, RegisterAppResult, RegisteredApp, } from './platform/apps.js';
|
|
36
41
|
export { requireHumanSession } from './platform/guard.js';
|
|
42
|
+
export type { JobHandle, PlatformModule, ProvisionProfile, ProvisionRun, ProvisionRunStatus, ProvisionStep, ProvisionStepStatus, ProvisionStoreInput, ProvisionStoreResult, WaitOptions, } from './platform/provision-store.js';
|
|
43
|
+
export { PROVISION_PROFILES } from './platform/provision-store.js';
|
|
37
44
|
export type { Redirect, RedirectCreateInput, RedirectsModule, RedirectUpdateInput, } from './seo/redirects.js';
|
|
38
45
|
export type { ChannelsModule } from './tenant/channels.js';
|
|
39
46
|
export type { ChannelRetention, RetentionHold, RetentionModule, SetRetentionInput, } from './tenant/retention.js';
|
package/dist/index.js
CHANGED
|
@@ -21,3 +21,4 @@ export { createAdminClient, } from './client.js';
|
|
|
21
21
|
export { AdminProjectionError } from './errors.js';
|
|
22
22
|
export { OPENING_HOURS_DAYS } from './locations/index.js';
|
|
23
23
|
export { requireHumanSession } from './platform/guard.js';
|
|
24
|
+
export { PROVISION_PROFILES } from './platform/provision-store.js';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Transport } from '@sixtyninecommerce/core';
|
|
2
|
+
import { type App } from '../apps/index.js';
|
|
3
|
+
import type { AdminClientConfig } from '../client.js';
|
|
4
|
+
import type { Call } from '../types.js';
|
|
5
|
+
export declare const APPS_QUERY = "\n query Apps($options: AppListOptions) {\n apps(options: $options) {\n totalItems\n items { \n \n id\n slug\n name\n ownerLabel\n declaredPermissions\n declaredEvents\n declaredFieldOwnership\n customerData\n customerDataPurpose\n metafieldDefinitions\n configSchema\n hooks\n headTemplates\n sandboxOnly\n status\n manifestVersion\n createdAt\n updatedAt\n\n keys {\n id\n kid\n alg\n publicKey\n revokedAt\n createdAt\n }\n enabledChannelIds\n }\n }\n }\n";
|
|
6
|
+
export declare const APP_QUERY = "\n query App($id: ID!) {\n app(id: $id) { \n \n id\n slug\n name\n ownerLabel\n declaredPermissions\n declaredEvents\n declaredFieldOwnership\n customerData\n customerDataPurpose\n metafieldDefinitions\n configSchema\n hooks\n headTemplates\n sandboxOnly\n status\n manifestVersion\n createdAt\n updatedAt\n\n keys {\n id\n kid\n alg\n publicKey\n revokedAt\n createdAt\n }\n enabledChannelIds\n }\n }\n";
|
|
7
|
+
export declare const REGISTER_MUTATION = "\n mutation RegisterApp($manifest: JSON!) {\n registerApp(manifest: $manifest) { \n created\n consentRequiredInstallationIds\n consentReasons\n app { \n \n id\n slug\n name\n ownerLabel\n declaredPermissions\n declaredEvents\n declaredFieldOwnership\n customerData\n customerDataPurpose\n metafieldDefinitions\n configSchema\n hooks\n headTemplates\n sandboxOnly\n status\n manifestVersion\n createdAt\n updatedAt\n\n keys {\n id\n kid\n alg\n publicKey\n revokedAt\n createdAt\n }\n enabledChannelIds\n }\n }\n }\n";
|
|
8
|
+
export declare const UPDATE_MUTATION = "\n mutation UpdateApp($id: ID!, $manifest: JSON!) {\n updateApp(id: $id, manifest: $manifest) { \n created\n consentRequiredInstallationIds\n consentReasons\n app { \n \n id\n slug\n name\n ownerLabel\n declaredPermissions\n declaredEvents\n declaredFieldOwnership\n customerData\n customerDataPurpose\n metafieldDefinitions\n configSchema\n hooks\n headTemplates\n sandboxOnly\n status\n manifestVersion\n createdAt\n updatedAt\n\n keys {\n id\n kid\n alg\n publicKey\n revokedAt\n createdAt\n }\n enabledChannelIds\n }\n }\n }\n";
|
|
9
|
+
export declare const ADD_KEY_MUTATION = "\n mutation AddAppKey($appId: ID!, $kid: String!, $alg: String!, $publicKey: String!) {\n addAppKey(appId: $appId, kid: $kid, alg: $alg, publicKey: $publicKey) {\n id\n kid\n alg\n publicKey\n revokedAt\n createdAt\n }\n }\n";
|
|
10
|
+
export declare const REVOKE_KEY_MUTATION = "\n mutation RevokeAppKey($appId: ID!, $kid: String!) {\n revokeAppKey(appId: $appId, kid: $kid) {\n id\n kid\n alg\n revokedAt\n }\n }\n";
|
|
11
|
+
export declare const SUSPEND_MUTATION = "\n mutation SuspendApp($id: ID!) {\n suspendApp(id: $id) { \n \n id\n slug\n name\n ownerLabel\n declaredPermissions\n declaredEvents\n declaredFieldOwnership\n customerData\n customerDataPurpose\n metafieldDefinitions\n configSchema\n hooks\n headTemplates\n sandboxOnly\n status\n manifestVersion\n createdAt\n updatedAt\n\n keys {\n id\n kid\n alg\n publicKey\n revokedAt\n createdAt\n }\n enabledChannelIds\n }\n }\n";
|
|
12
|
+
export declare const RESUME_MUTATION = "\n mutation ResumeApp($id: ID!) {\n resumeApp(id: $id) { \n \n id\n slug\n name\n ownerLabel\n declaredPermissions\n declaredEvents\n declaredFieldOwnership\n customerData\n customerDataPurpose\n metafieldDefinitions\n configSchema\n hooks\n headTemplates\n sandboxOnly\n status\n manifestVersion\n createdAt\n updatedAt\n\n keys {\n id\n kid\n alg\n publicKey\n revokedAt\n createdAt\n }\n enabledChannelIds\n }\n }\n";
|
|
13
|
+
export declare const ENABLE_MUTATION = "\n mutation EnableAppForTenant($appId: ID!, $channelId: ID!) {\n enableAppForTenant(appId: $appId, channelId: $channelId) { \n \n id\n slug\n name\n ownerLabel\n declaredPermissions\n declaredEvents\n declaredFieldOwnership\n customerData\n customerDataPurpose\n metafieldDefinitions\n configSchema\n hooks\n headTemplates\n sandboxOnly\n status\n manifestVersion\n createdAt\n updatedAt\n\n keys {\n id\n kid\n alg\n publicKey\n revokedAt\n createdAt\n }\n enabledChannelIds\n }\n }\n";
|
|
14
|
+
export declare const DISABLE_MUTATION = "\n mutation DisableAppForTenant($appId: ID!, $channelId: ID!) {\n disableAppForTenant(appId: $appId, channelId: $channelId) { \n \n id\n slug\n name\n ownerLabel\n declaredPermissions\n declaredEvents\n declaredFieldOwnership\n customerData\n customerDataPurpose\n metafieldDefinitions\n configSchema\n hooks\n headTemplates\n sandboxOnly\n status\n manifestVersion\n createdAt\n updatedAt\n\n keys {\n id\n kid\n alg\n publicKey\n revokedAt\n createdAt\n }\n enabledChannelIds\n }\n }\n";
|
|
15
|
+
/** A registered public key. Only public material is ever returned. */
|
|
16
|
+
export interface AppKey {
|
|
17
|
+
id: string;
|
|
18
|
+
kid: string;
|
|
19
|
+
/** `EdDSA` | `RS256`. */
|
|
20
|
+
alg: string;
|
|
21
|
+
publicKey: string;
|
|
22
|
+
revokedAt: string | null;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
}
|
|
25
|
+
export interface RegisteredApp extends App {
|
|
26
|
+
keys: AppKey[];
|
|
27
|
+
/** Channels a SuperAdmin has enabled this app for. */
|
|
28
|
+
enabledChannelIds: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface RegisterAppResult {
|
|
31
|
+
app: RegisteredApp;
|
|
32
|
+
/** False when the manifest updated an app that was already registered. */
|
|
33
|
+
created: boolean;
|
|
34
|
+
/** Installations this registration moved to `needs_consent`. */
|
|
35
|
+
consentRequiredInstallationIds: string[];
|
|
36
|
+
/** Why, in the words their approvers will read. */
|
|
37
|
+
consentReasons: string[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The app REGISTRY — the platform operator's surface (headless spec §6.3, §6.12).
|
|
41
|
+
*
|
|
42
|
+
* Human SuperAdmin session only, enforced twice over: the engine gates every
|
|
43
|
+
* operation on Vendure's `SuperAdmin` permission — not a custom one, because a
|
|
44
|
+
* custom permission is evaluated against the ACTIVE channel and every mutation
|
|
45
|
+
* here is platform-wide — and the SDK refuses to put a machine credential on the
|
|
46
|
+
* wire towards it at all (`requireHumanSession`). An app that could register
|
|
47
|
+
* apps could declare anything and enable itself.
|
|
48
|
+
*/
|
|
49
|
+
export interface PlatformAppsModule {
|
|
50
|
+
list(options?: {
|
|
51
|
+
skip?: number;
|
|
52
|
+
take?: number;
|
|
53
|
+
}, call?: Call): Promise<{
|
|
54
|
+
items: RegisteredApp[];
|
|
55
|
+
totalItems: number;
|
|
56
|
+
}>;
|
|
57
|
+
get(id: string, call?: Call): Promise<RegisteredApp | null>;
|
|
58
|
+
/** Ingest a `sixtynine.app.json`. Upserts by slug: a known slug is an update. */
|
|
59
|
+
register(manifest: Record<string, unknown>, call?: Call): Promise<RegisterAppResult>;
|
|
60
|
+
/** The same ingest addressed by id; refuses a manifest whose slug is another app's. */
|
|
61
|
+
update(id: string, manifest: Record<string, unknown>, call?: Call): Promise<RegisterAppResult>;
|
|
62
|
+
addKey(input: {
|
|
63
|
+
appId: string;
|
|
64
|
+
kid: string;
|
|
65
|
+
alg: string;
|
|
66
|
+
publicKey: string;
|
|
67
|
+
}, call?: Call): Promise<AppKey>;
|
|
68
|
+
revokeKey(appId: string, kid: string, call?: Call): Promise<AppKey>;
|
|
69
|
+
/** Suspend platform-wide and kill every installation token in one action. */
|
|
70
|
+
suspend(id: string, call?: Call): Promise<RegisteredApp>;
|
|
71
|
+
resume(id: string, call?: Call): Promise<RegisteredApp>;
|
|
72
|
+
/** Offer an app to one tenant. A tenant admin can only install what is enabled. */
|
|
73
|
+
enableForTenant(appId: string, channelId: string, call?: Call): Promise<RegisteredApp>;
|
|
74
|
+
disableForTenant(appId: string, channelId: string, call?: Call): Promise<RegisteredApp>;
|
|
75
|
+
}
|
|
76
|
+
export declare function createPlatformAppsModule(transport: Transport, config: AdminClientConfig): PlatformAppsModule;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { APP_FIELDS } from '../apps/index.js';
|
|
2
|
+
import { requireHumanSession } from './guard.js';
|
|
3
|
+
const APP_WITH_KEYS_FIELDS = /* GraphQL */ `
|
|
4
|
+
${APP_FIELDS}
|
|
5
|
+
keys {
|
|
6
|
+
id
|
|
7
|
+
kid
|
|
8
|
+
alg
|
|
9
|
+
publicKey
|
|
10
|
+
revokedAt
|
|
11
|
+
createdAt
|
|
12
|
+
}
|
|
13
|
+
enabledChannelIds
|
|
14
|
+
`;
|
|
15
|
+
const REGISTER_RESULT_FIELDS = /* GraphQL */ `
|
|
16
|
+
created
|
|
17
|
+
consentRequiredInstallationIds
|
|
18
|
+
consentReasons
|
|
19
|
+
app { ${APP_WITH_KEYS_FIELDS} }
|
|
20
|
+
`;
|
|
21
|
+
export const APPS_QUERY = /* GraphQL */ `
|
|
22
|
+
query Apps($options: AppListOptions) {
|
|
23
|
+
apps(options: $options) {
|
|
24
|
+
totalItems
|
|
25
|
+
items { ${APP_WITH_KEYS_FIELDS} }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
`;
|
|
29
|
+
export const APP_QUERY = /* GraphQL */ `
|
|
30
|
+
query App($id: ID!) {
|
|
31
|
+
app(id: $id) { ${APP_WITH_KEYS_FIELDS} }
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
export const REGISTER_MUTATION = /* GraphQL */ `
|
|
35
|
+
mutation RegisterApp($manifest: JSON!) {
|
|
36
|
+
registerApp(manifest: $manifest) { ${REGISTER_RESULT_FIELDS} }
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
export const UPDATE_MUTATION = /* GraphQL */ `
|
|
40
|
+
mutation UpdateApp($id: ID!, $manifest: JSON!) {
|
|
41
|
+
updateApp(id: $id, manifest: $manifest) { ${REGISTER_RESULT_FIELDS} }
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
export const ADD_KEY_MUTATION = /* GraphQL */ `
|
|
45
|
+
mutation AddAppKey($appId: ID!, $kid: String!, $alg: String!, $publicKey: String!) {
|
|
46
|
+
addAppKey(appId: $appId, kid: $kid, alg: $alg, publicKey: $publicKey) {
|
|
47
|
+
id
|
|
48
|
+
kid
|
|
49
|
+
alg
|
|
50
|
+
publicKey
|
|
51
|
+
revokedAt
|
|
52
|
+
createdAt
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
`;
|
|
56
|
+
export const REVOKE_KEY_MUTATION = /* GraphQL */ `
|
|
57
|
+
mutation RevokeAppKey($appId: ID!, $kid: String!) {
|
|
58
|
+
revokeAppKey(appId: $appId, kid: $kid) {
|
|
59
|
+
id
|
|
60
|
+
kid
|
|
61
|
+
alg
|
|
62
|
+
revokedAt
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
export const SUSPEND_MUTATION = /* GraphQL */ `
|
|
67
|
+
mutation SuspendApp($id: ID!) {
|
|
68
|
+
suspendApp(id: $id) { ${APP_WITH_KEYS_FIELDS} }
|
|
69
|
+
}
|
|
70
|
+
`;
|
|
71
|
+
export const RESUME_MUTATION = /* GraphQL */ `
|
|
72
|
+
mutation ResumeApp($id: ID!) {
|
|
73
|
+
resumeApp(id: $id) { ${APP_WITH_KEYS_FIELDS} }
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
export const ENABLE_MUTATION = /* GraphQL */ `
|
|
77
|
+
mutation EnableAppForTenant($appId: ID!, $channelId: ID!) {
|
|
78
|
+
enableAppForTenant(appId: $appId, channelId: $channelId) { ${APP_WITH_KEYS_FIELDS} }
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
export const DISABLE_MUTATION = /* GraphQL */ `
|
|
82
|
+
mutation DisableAppForTenant($appId: ID!, $channelId: ID!) {
|
|
83
|
+
disableAppForTenant(appId: $appId, channelId: $channelId) { ${APP_WITH_KEYS_FIELDS} }
|
|
84
|
+
}
|
|
85
|
+
`;
|
|
86
|
+
export function createPlatformAppsModule(transport, config) {
|
|
87
|
+
const guarded = async (run) => {
|
|
88
|
+
requireHumanSession(config);
|
|
89
|
+
return run();
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
list(options, call) {
|
|
93
|
+
return guarded(async () => {
|
|
94
|
+
const data = await transport.graphql({ api: 'admin', query: APPS_QUERY, variables: { options: options ?? null } }, call);
|
|
95
|
+
return data.apps;
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
get(id, call) {
|
|
99
|
+
return guarded(async () => {
|
|
100
|
+
const data = await transport.graphql({ api: 'admin', query: APP_QUERY, variables: { id } }, call);
|
|
101
|
+
return data.app;
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
register(manifest, call) {
|
|
105
|
+
return guarded(async () => {
|
|
106
|
+
const data = await transport.graphql({ api: 'admin', query: REGISTER_MUTATION, variables: { manifest } }, call);
|
|
107
|
+
return data.registerApp;
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
update(id, manifest, call) {
|
|
111
|
+
return guarded(async () => {
|
|
112
|
+
const data = await transport.graphql({ api: 'admin', query: UPDATE_MUTATION, variables: { id, manifest } }, call);
|
|
113
|
+
return data.updateApp;
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
addKey(input, call) {
|
|
117
|
+
return guarded(async () => {
|
|
118
|
+
const data = await transport.graphql({ api: 'admin', query: ADD_KEY_MUTATION, variables: input }, call);
|
|
119
|
+
return data.addAppKey;
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
revokeKey(appId, kid, call) {
|
|
123
|
+
return guarded(async () => {
|
|
124
|
+
const data = await transport.graphql({ api: 'admin', query: REVOKE_KEY_MUTATION, variables: { appId, kid } }, call);
|
|
125
|
+
return data.revokeAppKey;
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
suspend(id, call) {
|
|
129
|
+
return guarded(async () => {
|
|
130
|
+
const data = await transport.graphql({ api: 'admin', query: SUSPEND_MUTATION, variables: { id } }, call);
|
|
131
|
+
return data.suspendApp;
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
resume(id, call) {
|
|
135
|
+
return guarded(async () => {
|
|
136
|
+
const data = await transport.graphql({ api: 'admin', query: RESUME_MUTATION, variables: { id } }, call);
|
|
137
|
+
return data.resumeApp;
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
enableForTenant(appId, channelId, call) {
|
|
141
|
+
return guarded(async () => {
|
|
142
|
+
const data = await transport.graphql({ api: 'admin', query: ENABLE_MUTATION, variables: { appId, channelId } }, call);
|
|
143
|
+
return data.enableAppForTenant;
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
disableForTenant(appId, channelId, call) {
|
|
147
|
+
return guarded(async () => {
|
|
148
|
+
const data = await transport.graphql({ api: 'admin', query: DISABLE_MUTATION, variables: { appId, channelId } }, call);
|
|
149
|
+
return data.disableAppForTenant;
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { type Transport } from '@sixtyninecommerce/core';
|
|
2
|
+
import type { AdminClientConfig } from '../client.js';
|
|
3
|
+
import type { Call } from '../types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Which objects a new store is provisioned with.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors the server's list; the server is the enforcer, so an unknown value is
|
|
8
|
+
* refused with `VALIDATION_FAILED` naming the accepted ones rather than failing
|
|
9
|
+
* here.
|
|
10
|
+
*/
|
|
11
|
+
export type ProvisionProfile = 'cms-only' | 'catalog' | 'commerce' | 'pos-commerce' | 'full';
|
|
12
|
+
/** Every profile, for a caller that renders a picker. */
|
|
13
|
+
export declare const PROVISION_PROFILES: readonly ProvisionProfile[];
|
|
14
|
+
export interface ProvisionStoreInput {
|
|
15
|
+
/** Human store name — becomes the Seller name and the store's business name. */
|
|
16
|
+
name: string;
|
|
17
|
+
/**
|
|
18
|
+
* Lowercase kebab-case. It is also the object-storage prefix, the CMS key and
|
|
19
|
+
* the order-number namespace, and it can only be changed later by a rename
|
|
20
|
+
* job — so it is worth getting right.
|
|
21
|
+
*/
|
|
22
|
+
code: string;
|
|
23
|
+
/** Defaults to `commerce`, which is the full webshop. */
|
|
24
|
+
profile?: ProvisionProfile;
|
|
25
|
+
defaultLanguageCode: string;
|
|
26
|
+
availableLanguageCodes?: string[];
|
|
27
|
+
defaultCurrencyCode: string;
|
|
28
|
+
availableCurrencyCodes?: string[];
|
|
29
|
+
pricesIncludeTax?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The store's tax and shipping zones. Send BOTH or NEITHER — a request naming
|
|
32
|
+
* one is refused, because the other would then be derived and the store would
|
|
33
|
+
* quietly end up with two different zones.
|
|
34
|
+
*
|
|
35
|
+
* Omitted, both are resolved from `country`: the SMALLEST zone that actually
|
|
36
|
+
* CONTAINS that country wins (ties broken by name, then id). A country in no
|
|
37
|
+
* zone is an error, not a guess — the old heuristic matched zone NAMES and
|
|
38
|
+
* fell back to Europe/EU, which is how a US store ended up on European VAT.
|
|
39
|
+
*/
|
|
40
|
+
defaultTaxZoneId?: string;
|
|
41
|
+
defaultShippingZoneId?: string;
|
|
42
|
+
addressLine1: string;
|
|
43
|
+
addressLine2?: string;
|
|
44
|
+
city: string;
|
|
45
|
+
postalCode: string;
|
|
46
|
+
province?: string;
|
|
47
|
+
/** ISO 3166-1 alpha-2. */
|
|
48
|
+
country: string;
|
|
49
|
+
/** IANA zone. Every day-resolution comparison for this store uses it. */
|
|
50
|
+
timezone: string;
|
|
51
|
+
legalName: string;
|
|
52
|
+
vatNumber?: string;
|
|
53
|
+
registrationNumber?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Invite a merchant administrator to the new store. SOFT-FAIL: a bounced
|
|
56
|
+
* invitation never fails the provisioning — it lands in
|
|
57
|
+
* {@link ProvisionRun.warnings} and the store is still built.
|
|
58
|
+
*/
|
|
59
|
+
invite?: {
|
|
60
|
+
email: string;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Reserved. Refused today with `SANDBOX_NOT_AVAILABLE` (a
|
|
64
|
+
* {@link ValidationError}) until the sandbox channel flag ships.
|
|
65
|
+
*/
|
|
66
|
+
sandbox?: boolean;
|
|
67
|
+
/** An organization code. Refused with `ORGANIZATIONS_NOT_AVAILABLE`. */
|
|
68
|
+
organization?: string;
|
|
69
|
+
/** A Seller id. Refused with `ORGANIZATIONS_NOT_AVAILABLE`. */
|
|
70
|
+
legalEntity?: string;
|
|
71
|
+
}
|
|
72
|
+
export type ProvisionRunStatus = 'queued' | 'running' | 'succeeded' | 'failed' | 'compensated';
|
|
73
|
+
export type ProvisionStepStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'skipped' | 'compensated';
|
|
74
|
+
export interface ProvisionStep {
|
|
75
|
+
/** `zones`, `seller`, `channel`, `shipping-methods`, `stock-location`, `roles`, `cms-channel`, `cms-content`, `invite`. */
|
|
76
|
+
name: string;
|
|
77
|
+
/** `skipped` is a DECISION — the profile did not ask for this object. */
|
|
78
|
+
status: ProvisionStepStatus;
|
|
79
|
+
/** ISO-8601 UTC, or null for a step that never started. */
|
|
80
|
+
startedAt: string | null;
|
|
81
|
+
finishedAt: string | null;
|
|
82
|
+
error?: string;
|
|
83
|
+
}
|
|
84
|
+
export interface ProvisionStoreResult {
|
|
85
|
+
channelId: string;
|
|
86
|
+
channelCode: string;
|
|
87
|
+
/** The new store's channel token — what a storefront and this SDK address it by. */
|
|
88
|
+
channelToken: string;
|
|
89
|
+
sellerId: string;
|
|
90
|
+
/** The role a merchant invitation attaches to. */
|
|
91
|
+
adminRoleId: string;
|
|
92
|
+
}
|
|
93
|
+
export interface ProvisionRun {
|
|
94
|
+
id: string;
|
|
95
|
+
status: ProvisionRunStatus;
|
|
96
|
+
/** The step running now, or the one that failed. Null when nothing is running. */
|
|
97
|
+
step: string | null;
|
|
98
|
+
profile: string;
|
|
99
|
+
channelCode: string;
|
|
100
|
+
steps: ProvisionStep[];
|
|
101
|
+
/** Present only on a `succeeded` run. */
|
|
102
|
+
result?: ProvisionStoreResult;
|
|
103
|
+
/** Non-fatal notes. A merchant invite that bounced is the only one today. */
|
|
104
|
+
warnings: string[];
|
|
105
|
+
error?: string;
|
|
106
|
+
createdAt: string;
|
|
107
|
+
startedAt: string | null;
|
|
108
|
+
completedAt: string | null;
|
|
109
|
+
[extra: string]: unknown;
|
|
110
|
+
}
|
|
111
|
+
export interface WaitOptions {
|
|
112
|
+
/** How often to poll. Default 2000 ms; anything below 250 ms is raised to it. */
|
|
113
|
+
intervalMs?: number;
|
|
114
|
+
/** Give up after this long. Default 300000 ms (5 minutes). */
|
|
115
|
+
timeoutMs?: number;
|
|
116
|
+
signal?: AbortSignal;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A long-running operation in flight (spec §6.11). `runId` is the receipt: it
|
|
120
|
+
* outlives this process, so a caller that cannot wait can hand it on and poll
|
|
121
|
+
* `provisionRun(runId)` later.
|
|
122
|
+
*/
|
|
123
|
+
export interface JobHandle<T> {
|
|
124
|
+
runId: string;
|
|
125
|
+
/**
|
|
126
|
+
* Poll until the run reaches a terminal status.
|
|
127
|
+
*
|
|
128
|
+
* Resolves with the result on success. Throws `PROVISION_RUN_COMPENSATED`
|
|
129
|
+
* when the run failed and the platform undid everything it had built (no
|
|
130
|
+
* store, no leftovers — retrying is safe), and `PROVISION_RUN_FAILED` when
|
|
131
|
+
* the cleanup ITSELF could not finish, whose message names what is left
|
|
132
|
+
* behind for an operator. `PROVISION_RUN_TIMEOUT` means only that waiting
|
|
133
|
+
* stopped: the run is still going, and {@link runId} still resolves it.
|
|
134
|
+
*/
|
|
135
|
+
wait(options?: WaitOptions): Promise<T>;
|
|
136
|
+
}
|
|
137
|
+
export interface PlatformModule {
|
|
138
|
+
/**
|
|
139
|
+
* Build a whole store: seller, channel, zones, shipping methods, stock
|
|
140
|
+
* location, roles, CMS registration and the default CMS menus and forms —
|
|
141
|
+
* which of them depends on `profile`.
|
|
142
|
+
*
|
|
143
|
+
* Returns as soon as the run is accepted. The work runs server-side as a saga
|
|
144
|
+
* with recorded compensation, so a failure half-way leaves nothing behind.
|
|
145
|
+
*
|
|
146
|
+
* `options.idempotencyKey` makes a retry return the run already in flight
|
|
147
|
+
* instead of starting a second one — send one, and reuse it only for retries
|
|
148
|
+
* of that same call.
|
|
149
|
+
*
|
|
150
|
+
* Needs a human SuperAdmin session: an API token is refused before it leaves
|
|
151
|
+
* the process (`PLATFORM_REQUIRES_SESSION`).
|
|
152
|
+
*/
|
|
153
|
+
provisionStore(input: ProvisionStoreInput, options?: Call): Promise<JobHandle<ProvisionStoreResult>>;
|
|
154
|
+
/** One provisioning run, by the id `provisionStore` returned. */
|
|
155
|
+
provisionRun(runId: string, options?: Call): Promise<ProvisionRun>;
|
|
156
|
+
}
|
|
157
|
+
export declare function createPlatformModule(transport: Transport, config: AdminClientConfig): PlatformModule;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { BusinessRuleError, TransportError } from '@sixtyninecommerce/core';
|
|
2
|
+
import { requireHumanSession } from './guard.js';
|
|
3
|
+
/** Every profile, for a caller that renders a picker. */
|
|
4
|
+
export const PROVISION_PROFILES = [
|
|
5
|
+
'cms-only',
|
|
6
|
+
'catalog',
|
|
7
|
+
'commerce',
|
|
8
|
+
'pos-commerce',
|
|
9
|
+
'full',
|
|
10
|
+
];
|
|
11
|
+
const TERMINAL = new Set(['succeeded', 'failed', 'compensated']);
|
|
12
|
+
const DEFAULT_INTERVAL_MS = 2_000;
|
|
13
|
+
const MIN_INTERVAL_MS = 250;
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
15
|
+
/**
|
|
16
|
+
* Consecutive failed READS tolerated while waiting. A poll is a read of a run
|
|
17
|
+
* that is still building a store: one 503 from a rolling restart must not
|
|
18
|
+
* report a failure for work that is going fine. Reset by any successful read,
|
|
19
|
+
* so only a sustained outage gives up — and a refusal that is ABOUT the run
|
|
20
|
+
* (not found, no permission) is not retried at all.
|
|
21
|
+
*/
|
|
22
|
+
const MAX_CONSECUTIVE_POLL_FAILURES = 3;
|
|
23
|
+
/** A read that failed for a reason polling again cannot fix. */
|
|
24
|
+
function isTerminalPollError(error) {
|
|
25
|
+
const status = error?.status;
|
|
26
|
+
return typeof status === 'number' && status >= 400 && status < 500 && status !== 429;
|
|
27
|
+
}
|
|
28
|
+
export function createPlatformModule(transport, config) {
|
|
29
|
+
async function provisionRun(runId, options) {
|
|
30
|
+
requireHumanSession(config);
|
|
31
|
+
return transport.rest('plugins', { method: 'GET', path: `/platform/provision-runs/${encodeURIComponent(runId)}` },
|
|
32
|
+
// Provisioning is cross-tenant: it creates a channel that does not exist
|
|
33
|
+
// yet, so the call must not be scoped to one.
|
|
34
|
+
{ ...options, allowUnscoped: true });
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
provisionRun,
|
|
38
|
+
async provisionStore(input, options) {
|
|
39
|
+
requireHumanSession(config);
|
|
40
|
+
const accepted = await transport.rest('plugins', { method: 'POST', path: '/platform/provision-store', body: input }, { ...options, allowUnscoped: true });
|
|
41
|
+
const runId = accepted.runId;
|
|
42
|
+
return {
|
|
43
|
+
runId,
|
|
44
|
+
async wait(waitOptions) {
|
|
45
|
+
const intervalMs = Math.max(waitOptions?.intervalMs ?? DEFAULT_INTERVAL_MS, MIN_INTERVAL_MS);
|
|
46
|
+
const deadline = Date.now() + (waitOptions?.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
47
|
+
let consecutiveFailures = 0;
|
|
48
|
+
let lastSeen;
|
|
49
|
+
for (;;) {
|
|
50
|
+
let run;
|
|
51
|
+
try {
|
|
52
|
+
run = await provisionRun(runId, {
|
|
53
|
+
...options,
|
|
54
|
+
...(waitOptions?.signal ? { signal: waitOptions.signal } : {}),
|
|
55
|
+
});
|
|
56
|
+
consecutiveFailures = 0;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (waitOptions?.signal?.aborted === true)
|
|
60
|
+
throw error;
|
|
61
|
+
consecutiveFailures += 1;
|
|
62
|
+
if (isTerminalPollError(error) ||
|
|
63
|
+
consecutiveFailures > MAX_CONSECUTIVE_POLL_FAILURES) {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
if (Date.now() >= deadline)
|
|
67
|
+
throw timedOut(runId, lastSeen);
|
|
68
|
+
await delay(intervalMs, waitOptions?.signal);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
lastSeen = run;
|
|
72
|
+
if (TERMINAL.has(run.status))
|
|
73
|
+
return resultOf(run);
|
|
74
|
+
if (Date.now() >= deadline)
|
|
75
|
+
throw timedOut(runId, run);
|
|
76
|
+
await delay(intervalMs, waitOptions?.signal);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function resultOf(run) {
|
|
84
|
+
if (run.status === 'succeeded') {
|
|
85
|
+
if (run.result)
|
|
86
|
+
return run.result;
|
|
87
|
+
throw new BusinessRuleError({
|
|
88
|
+
code: 'PROVISION_RUN_INCOMPLETE',
|
|
89
|
+
message: `Provisioning run ${run.id} reports success but carries no store. Read the run for its steps.`,
|
|
90
|
+
status: 422,
|
|
91
|
+
service: 'plugins',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
// `compensated` and `failed` differ in ONE way that matters to a caller:
|
|
95
|
+
// whether anything was left behind. Retrying is safe after the first and
|
|
96
|
+
// needs an operator after the second, so they are different codes.
|
|
97
|
+
//
|
|
98
|
+
// The detail is the failing step's own words — usually a service's message,
|
|
99
|
+
// which may or may not be a sentence — so it is punctuated here rather than
|
|
100
|
+
// run into the sentence that follows it.
|
|
101
|
+
const raw = run.error ?? `Provisioning failed at step "${run.step ?? 'unknown'}"`;
|
|
102
|
+
const detail = /[.!?]$/.test(raw.trim()) ? raw.trim() : `${raw.trim()}.`;
|
|
103
|
+
throw new BusinessRuleError({
|
|
104
|
+
code: run.status === 'compensated' ? 'PROVISION_RUN_COMPENSATED' : 'PROVISION_RUN_FAILED',
|
|
105
|
+
message: run.status === 'compensated'
|
|
106
|
+
? `${detail} Everything it had built was removed, so nothing is left behind. (That does not mean the code is free: the most common cause is a code another store already owns.)`
|
|
107
|
+
: `${detail} The cleanup did not finish — an operator has to remove what the message names.`,
|
|
108
|
+
status: 422,
|
|
109
|
+
service: 'plugins',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function timedOut(runId, run) {
|
|
113
|
+
const at = run
|
|
114
|
+
? `while it was at "${run.step ?? run.status}"`
|
|
115
|
+
: 'without reading it successfully';
|
|
116
|
+
return new TransportError({
|
|
117
|
+
code: 'PROVISION_RUN_TIMEOUT',
|
|
118
|
+
message: `Stopped waiting for provisioning run ${runId} ${at}. The run may still be going — poll platform.provisionRun("${runId}") to follow it.`,
|
|
119
|
+
status: 0,
|
|
120
|
+
service: 'plugins',
|
|
121
|
+
retryable: true,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function delay(ms, signal) {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
if (signal?.aborted) {
|
|
127
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error('Aborted'));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const timer = setTimeout(() => {
|
|
131
|
+
signal?.removeEventListener('abort', onAbort);
|
|
132
|
+
resolve();
|
|
133
|
+
}, ms);
|
|
134
|
+
function onAbort() {
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
reject(signal?.reason instanceof Error ? signal.reason : new Error('Aborted'));
|
|
137
|
+
}
|
|
138
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
139
|
+
});
|
|
140
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -6,12 +6,15 @@ export interface Call {
|
|
|
6
6
|
* the Admin API has no header for it, so an `apiTokens.*` write is never
|
|
7
7
|
* deduplicated and a repeat is a second write.
|
|
8
8
|
*
|
|
9
|
-
* On REST,
|
|
10
|
-
* `content.forms.upsertBySlug` (`PUT /forms/by-slug/:slug`)
|
|
11
|
-
* `seo.redirects.create` (`POST /redirects`)
|
|
12
|
-
*
|
|
13
|
-
* same key
|
|
14
|
-
*
|
|
9
|
+
* On REST, three of this SDK's writes are deduplicated by the service itself:
|
|
10
|
+
* `content.forms.upsertBySlug` (`PUT /forms/by-slug/:slug`),
|
|
11
|
+
* `seo.redirects.create` (`POST /redirects`) and
|
|
12
|
+
* `platform.provisionStore` (`POST /platform/provision-store`). Repeating one
|
|
13
|
+
* under the same key replays the first response instead of running it again
|
|
14
|
+
* — for `provisionStore` that means the run already in flight, not a second
|
|
15
|
+
* store on a code that is now taken; sending the same key with a DIFFERENT
|
|
16
|
+
* body is refused with 409. Everything else records the key on the wire and
|
|
17
|
+
* acts on nothing.
|
|
15
18
|
*
|
|
16
19
|
* Use a fresh key per intended write — a UUIDv7 is ideal — and reuse it only
|
|
17
20
|
* when retrying that same write. This SDK never retries a write on its own,
|
|
@@ -45,9 +48,12 @@ export interface Me {
|
|
|
45
48
|
}
|
|
46
49
|
/** What `auth.whoami()` reports about the calling credential. */
|
|
47
50
|
export interface WhoAmI {
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
/**
|
|
52
|
+
* A MACHINE principal (`api_token`, `app_installation`) never discloses the
|
|
53
|
+
* hidden administrator behind it.
|
|
54
|
+
*/
|
|
55
|
+
kind: 'api_token' | 'user' | 'app_installation';
|
|
56
|
+
/** Present only for a personal API token; null for every other principal. */
|
|
51
57
|
token: {
|
|
52
58
|
id: string;
|
|
53
59
|
name: string;
|
|
@@ -57,7 +63,27 @@ export interface WhoAmI {
|
|
|
57
63
|
lastUsedAt: string | null;
|
|
58
64
|
scopeKind: string;
|
|
59
65
|
} | null;
|
|
60
|
-
/** Present only for
|
|
66
|
+
/** Present only for `kind: 'app_installation'`; null otherwise. */
|
|
67
|
+
app: {
|
|
68
|
+
id: string;
|
|
69
|
+
slug: string;
|
|
70
|
+
name: string;
|
|
71
|
+
} | null;
|
|
72
|
+
/** Present only for `kind: 'app_installation'`; null otherwise. */
|
|
73
|
+
installation: {
|
|
74
|
+
id: string;
|
|
75
|
+
/** `channel` today; `organization` arrives with multi-region. */
|
|
76
|
+
targetKind: string;
|
|
77
|
+
targetId: string;
|
|
78
|
+
/**
|
|
79
|
+
* `active` | `needs_consent` | `suspended` | `uninstalled`. `needs_consent`
|
|
80
|
+
* is how an app learns the merchant has to approve declarations it has since
|
|
81
|
+
* grown — this credential keeps working with the OLD grants meanwhile.
|
|
82
|
+
*/
|
|
83
|
+
status: string;
|
|
84
|
+
consentVersion: number;
|
|
85
|
+
} | null;
|
|
86
|
+
/** Present only for a human session; null for a machine principal. */
|
|
61
87
|
user: {
|
|
62
88
|
id: string;
|
|
63
89
|
identifier: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sixtyninecommerce/admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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.2.
|
|
36
|
+
"@sixtyninecommerce/core": "^0.2.4"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/node": "22.19.7",
|