@sixtyninecommerce/admin 0.4.0 → 0.6.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 +125 -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 +24 -4
- package/dist/client.js +120 -8
- package/dist/index.d.ts +6 -1
- package/dist/platform/apps.d.ts +76 -0
- package/dist/platform/apps.js +153 -0
- package/dist/tenant/capabilities.d.ts +104 -0
- package/dist/tenant/capabilities.js +59 -0
- package/dist/types.d.ts +27 -4
- package/package.json +2 -2
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Transport } from '@sixtyninecommerce/core';
|
|
2
|
+
import type { Call } from '../types.js';
|
|
3
|
+
export declare const APP_FIELDS = "\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";
|
|
4
|
+
export declare const INSTALLATION_FIELDS = "\n id\n appId\n targetKind\n targetId\n grantedPermissions\n grantedFieldOwnership\n locationIds\n status\n consentVersion\n pendingConsentReasons\n installedAt\n uninstalledAt\n";
|
|
5
|
+
export declare const AVAILABLE_APPS_QUERY = "\n query AvailableApps {\n availableApps { \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 }\n";
|
|
6
|
+
export declare const MY_INSTALLATIONS_QUERY = "\n query MyInstallations {\n myInstallations { \n id\n appId\n targetKind\n targetId\n grantedPermissions\n grantedFieldOwnership\n locationIds\n status\n consentVersion\n pendingConsentReasons\n installedAt\n uninstalledAt\n }\n }\n";
|
|
7
|
+
export declare const INSTALLATION_QUERY = "\n query AppInstallation($id: ID!) {\n appInstallation(id: $id) { \n id\n appId\n targetKind\n targetId\n grantedPermissions\n grantedFieldOwnership\n locationIds\n status\n consentVersion\n pendingConsentReasons\n installedAt\n uninstalledAt\n }\n }\n";
|
|
8
|
+
export declare const INSTALL_MUTATION = "\n mutation InstallApp($input: InstallAppInput!) {\n installApp(input: $input) { \n id\n appId\n targetKind\n targetId\n grantedPermissions\n grantedFieldOwnership\n locationIds\n status\n consentVersion\n pendingConsentReasons\n installedAt\n uninstalledAt\n }\n }\n";
|
|
9
|
+
export declare const APPROVE_MUTATION = "\n mutation ApproveInstallationConsent($id: ID!, $input: ApproveInstallationConsentInput) {\n approveInstallationConsent(id: $id, input: $input) { \n id\n appId\n targetKind\n targetId\n grantedPermissions\n grantedFieldOwnership\n locationIds\n status\n consentVersion\n pendingConsentReasons\n installedAt\n uninstalledAt\n }\n }\n";
|
|
10
|
+
export declare const UNINSTALL_MUTATION = "\n mutation UninstallApp($id: ID!) {\n uninstallApp(id: $id) { \n id\n appId\n targetKind\n targetId\n grantedPermissions\n grantedFieldOwnership\n locationIds\n status\n consentVersion\n pendingConsentReasons\n installedAt\n uninstalledAt\n }\n }\n";
|
|
11
|
+
/** A registered app, as a tenant admin sees it on the consent screen. */
|
|
12
|
+
export interface App {
|
|
13
|
+
id: string;
|
|
14
|
+
slug: string;
|
|
15
|
+
name: string;
|
|
16
|
+
ownerLabel: string;
|
|
17
|
+
/** The ceiling on what an installation may be granted. */
|
|
18
|
+
declaredPermissions: string[];
|
|
19
|
+
declaredEvents: string[];
|
|
20
|
+
/** Normalised `"<entity>.<field>"` claims, e.g. `product.price`. */
|
|
21
|
+
declaredFieldOwnership: string[];
|
|
22
|
+
/** `none` | `identity` | `full`. */
|
|
23
|
+
customerData: string;
|
|
24
|
+
customerDataPurpose: string | null;
|
|
25
|
+
metafieldDefinitions: unknown[];
|
|
26
|
+
configSchema: Record<string, unknown> | null;
|
|
27
|
+
hooks: unknown[];
|
|
28
|
+
headTemplates: unknown[];
|
|
29
|
+
sandboxOnly: boolean;
|
|
30
|
+
/** `active` | `suspended`. */
|
|
31
|
+
status: string;
|
|
32
|
+
manifestVersion: number;
|
|
33
|
+
createdAt: string;
|
|
34
|
+
updatedAt: string;
|
|
35
|
+
}
|
|
36
|
+
export interface AppInstallation {
|
|
37
|
+
id: string;
|
|
38
|
+
appId: string;
|
|
39
|
+
/** `channel` today; `organization` arrives with multi-region. */
|
|
40
|
+
targetKind: string;
|
|
41
|
+
targetId: string;
|
|
42
|
+
grantedPermissions: string[];
|
|
43
|
+
/** Normalised `"<entity>.<field>"` claims, e.g. `product.price`. */
|
|
44
|
+
grantedFieldOwnership: string[];
|
|
45
|
+
/** Null means unrestricted. */
|
|
46
|
+
locationIds: string[] | null;
|
|
47
|
+
/** `active` | `needs_consent` | `suspended` | `uninstalled`. */
|
|
48
|
+
status: string;
|
|
49
|
+
consentVersion: number;
|
|
50
|
+
/** Why consent is pending, in the approver's words. Null unless `needs_consent`. */
|
|
51
|
+
pendingConsentReasons: string[] | null;
|
|
52
|
+
installedAt: string;
|
|
53
|
+
uninstalledAt: string | null;
|
|
54
|
+
}
|
|
55
|
+
/** What an approver agrees to. Every field omitted means "keep what was granted". */
|
|
56
|
+
export interface ApproveConsentInput {
|
|
57
|
+
grantedPermissions?: string[];
|
|
58
|
+
grantedFieldOwnership?: string[];
|
|
59
|
+
/**
|
|
60
|
+
* Replace the stock-location restriction. Omit to keep it; pass `null` to LIFT
|
|
61
|
+
* it entirely — the engine distinguishes the two, so a plain `string[]` would
|
|
62
|
+
* leave a tenant who restricted an app to one warehouse no way back short of
|
|
63
|
+
* uninstalling it.
|
|
64
|
+
*/
|
|
65
|
+
locationIds?: string[] | null;
|
|
66
|
+
}
|
|
67
|
+
export interface InstallAppInput {
|
|
68
|
+
appId: string;
|
|
69
|
+
/** Must be a subset of the app's `declaredPermissions`, and of the caller's own. */
|
|
70
|
+
grantedPermissions?: string[];
|
|
71
|
+
grantedFieldOwnership?: string[];
|
|
72
|
+
/** Restrict the installation to these stock locations. Omit for unrestricted. */
|
|
73
|
+
locationIds?: string[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Installing and managing apps on the caller's OWN store (headless spec §6.3).
|
|
77
|
+
*
|
|
78
|
+
* Needs `InstallApps` and a tenant context. Every method acts on the active
|
|
79
|
+
* channel — there is no channel parameter, because a channel-id input would have
|
|
80
|
+
* to be permission-checked against a store the caller may not belong to, and the
|
|
81
|
+
* engine refuses that shape for the same reason it does on API tokens.
|
|
82
|
+
*
|
|
83
|
+
* The REGISTRY side (registering apps, keys, suspension, enabling an app for a
|
|
84
|
+
* tenant) lives under `admin.platform.apps` and needs a human SuperAdmin session.
|
|
85
|
+
*/
|
|
86
|
+
export interface AppsModule {
|
|
87
|
+
/** Apps a platform operator has enabled for this store and that are not suspended. */
|
|
88
|
+
list(options?: Call): Promise<App[]>;
|
|
89
|
+
/** Consent to a subset of an app's declarations and install it. */
|
|
90
|
+
install(input: InstallAppInput, options?: Call): Promise<AppInstallation>;
|
|
91
|
+
/** This store's installations, newest first. */
|
|
92
|
+
installations(options?: Call): Promise<AppInstallation[]>;
|
|
93
|
+
installation(id: string, options?: Call): Promise<AppInstallation | null>;
|
|
94
|
+
/**
|
|
95
|
+
* Approve declarations that grew, granting what you name.
|
|
96
|
+
*
|
|
97
|
+
* Growth is granted only by naming it — omit `grants` and the installation
|
|
98
|
+
* keeps exactly what it had, which is the right default for an app that grew
|
|
99
|
+
* an event type or a metafield. Tokens already minted keep the OLD grants
|
|
100
|
+
* until this lands.
|
|
101
|
+
*/
|
|
102
|
+
approveConsent(id: string, grants?: ApproveConsentInput, options?: Call): Promise<AppInstallation>;
|
|
103
|
+
/** Revoke every token and end the grant. The row is kept as provenance. */
|
|
104
|
+
uninstall(id: string, options?: Call): Promise<AppInstallation>;
|
|
105
|
+
}
|
|
106
|
+
export declare function createAppsModule(transport: Transport): AppsModule;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export const APP_FIELDS = /* GraphQL */ `
|
|
2
|
+
id
|
|
3
|
+
slug
|
|
4
|
+
name
|
|
5
|
+
ownerLabel
|
|
6
|
+
declaredPermissions
|
|
7
|
+
declaredEvents
|
|
8
|
+
declaredFieldOwnership
|
|
9
|
+
customerData
|
|
10
|
+
customerDataPurpose
|
|
11
|
+
metafieldDefinitions
|
|
12
|
+
configSchema
|
|
13
|
+
hooks
|
|
14
|
+
headTemplates
|
|
15
|
+
sandboxOnly
|
|
16
|
+
status
|
|
17
|
+
manifestVersion
|
|
18
|
+
createdAt
|
|
19
|
+
updatedAt
|
|
20
|
+
`;
|
|
21
|
+
export const INSTALLATION_FIELDS = /* GraphQL */ `
|
|
22
|
+
id
|
|
23
|
+
appId
|
|
24
|
+
targetKind
|
|
25
|
+
targetId
|
|
26
|
+
grantedPermissions
|
|
27
|
+
grantedFieldOwnership
|
|
28
|
+
locationIds
|
|
29
|
+
status
|
|
30
|
+
consentVersion
|
|
31
|
+
pendingConsentReasons
|
|
32
|
+
installedAt
|
|
33
|
+
uninstalledAt
|
|
34
|
+
`;
|
|
35
|
+
export const AVAILABLE_APPS_QUERY = /* GraphQL */ `
|
|
36
|
+
query AvailableApps {
|
|
37
|
+
availableApps { ${APP_FIELDS} }
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
export const MY_INSTALLATIONS_QUERY = /* GraphQL */ `
|
|
41
|
+
query MyInstallations {
|
|
42
|
+
myInstallations { ${INSTALLATION_FIELDS} }
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
export const INSTALLATION_QUERY = /* GraphQL */ `
|
|
46
|
+
query AppInstallation($id: ID!) {
|
|
47
|
+
appInstallation(id: $id) { ${INSTALLATION_FIELDS} }
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
export const INSTALL_MUTATION = /* GraphQL */ `
|
|
51
|
+
mutation InstallApp($input: InstallAppInput!) {
|
|
52
|
+
installApp(input: $input) { ${INSTALLATION_FIELDS} }
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
export const APPROVE_MUTATION = /* GraphQL */ `
|
|
56
|
+
mutation ApproveInstallationConsent($id: ID!, $input: ApproveInstallationConsentInput) {
|
|
57
|
+
approveInstallationConsent(id: $id, input: $input) { ${INSTALLATION_FIELDS} }
|
|
58
|
+
}
|
|
59
|
+
`;
|
|
60
|
+
export const UNINSTALL_MUTATION = /* GraphQL */ `
|
|
61
|
+
mutation UninstallApp($id: ID!) {
|
|
62
|
+
uninstallApp(id: $id) { ${INSTALLATION_FIELDS} }
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
export function createAppsModule(transport) {
|
|
66
|
+
return {
|
|
67
|
+
async list(options) {
|
|
68
|
+
const data = await transport.graphql({ api: 'admin', query: AVAILABLE_APPS_QUERY }, options);
|
|
69
|
+
return data.availableApps;
|
|
70
|
+
},
|
|
71
|
+
async install(input, options) {
|
|
72
|
+
const data = await transport.graphql({ api: 'admin', query: INSTALL_MUTATION, variables: { input } }, options);
|
|
73
|
+
return data.installApp;
|
|
74
|
+
},
|
|
75
|
+
async installations(options) {
|
|
76
|
+
const data = await transport.graphql({ api: 'admin', query: MY_INSTALLATIONS_QUERY }, options);
|
|
77
|
+
return data.myInstallations;
|
|
78
|
+
},
|
|
79
|
+
async installation(id, options) {
|
|
80
|
+
const data = await transport.graphql({ api: 'admin', query: INSTALLATION_QUERY, variables: { id } }, options);
|
|
81
|
+
return data.appInstallation;
|
|
82
|
+
},
|
|
83
|
+
async approveConsent(id, grants, options) {
|
|
84
|
+
const data = await transport.graphql({ api: 'admin', query: APPROVE_MUTATION, variables: { id, input: grants ?? null } }, options);
|
|
85
|
+
return data.approveInstallationConsent;
|
|
86
|
+
},
|
|
87
|
+
async uninstall(id, options) {
|
|
88
|
+
const data = await transport.graphql({ api: 'admin', query: UNINSTALL_MUTATION, variables: { id } }, options);
|
|
89
|
+
return data.uninstallApp;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { type DeprecationNotice, type RateLimitInfo, type RetryPolicy, type TenantContext, type TenantScope } from '@sixtyninecommerce/core';
|
|
2
2
|
import { type ApiTokensModule } from './api-tokens/index.js';
|
|
3
|
+
import { type AppCredentialConfig } from './apps/credential.js';
|
|
4
|
+
import { type AppsModule } from './apps/index.js';
|
|
3
5
|
import { type AuditModule } from './audit/index.js';
|
|
4
6
|
import { type AuthModule } from './auth/index.js';
|
|
5
7
|
import { type FormsModule } from './content/forms.js';
|
|
6
8
|
import { type HeadsModule } from './heads/index.js';
|
|
7
9
|
import { type LocationsModule } from './locations/index.js';
|
|
8
10
|
import { type MetaModule } from './meta/index.js';
|
|
11
|
+
import { type PlatformAppsModule } from './platform/apps.js';
|
|
9
12
|
import { type PlatformModule } from './platform/provision-store.js';
|
|
10
13
|
import { type RedirectsModule } from './seo/redirects.js';
|
|
14
|
+
import { type CapabilitiesModule } from './tenant/capabilities.js';
|
|
11
15
|
import { type ChannelsModule } from './tenant/channels.js';
|
|
12
16
|
import { type RetentionModule } from './tenant/retention.js';
|
|
13
17
|
export interface AdminClientConfig {
|
|
@@ -33,7 +37,7 @@ export interface AdminClientConfig {
|
|
|
33
37
|
token: string;
|
|
34
38
|
} | {
|
|
35
39
|
session: SessionConfig;
|
|
36
|
-
};
|
|
40
|
+
} | AppAuthConfig;
|
|
37
41
|
tenant?: Partial<TenantContext> & {
|
|
38
42
|
scope?: TenantScope;
|
|
39
43
|
};
|
|
@@ -42,9 +46,21 @@ export interface AdminClientConfig {
|
|
|
42
46
|
timeoutMs?: number;
|
|
43
47
|
onDeprecation?: (notice: DeprecationNotice) => void;
|
|
44
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* An APP's own credential (headless spec §6.3). The SDK signs an assertion with
|
|
51
|
+
* the app's private key, exchanges it for an `s69i_` bearer, and refreshes that
|
|
52
|
+
* bearer a minute before it expires — an app never handles the token itself.
|
|
53
|
+
*/
|
|
54
|
+
export interface AppAuthConfig {
|
|
55
|
+
app: AppCredentialConfig;
|
|
56
|
+
/** The installation this credential acts as, from `admin.apps.install(...)`. */
|
|
57
|
+
installationId: string;
|
|
58
|
+
}
|
|
45
59
|
export interface AdminClient {
|
|
46
60
|
auth: AuthModule;
|
|
47
61
|
apiTokens: ApiTokensModule;
|
|
62
|
+
/** Installing and managing apps on the ACTIVE store. Needs `InstallApps`. */
|
|
63
|
+
apps: AppsModule;
|
|
48
64
|
audit: AuditModule;
|
|
49
65
|
channels: ChannelsModule;
|
|
50
66
|
content: {
|
|
@@ -61,13 +77,17 @@ export interface AdminClient {
|
|
|
61
77
|
};
|
|
62
78
|
/** The calling tenant's own settings. Always the ACTIVE channel — no channel argument. */
|
|
63
79
|
tenant: {
|
|
80
|
+
capabilities: CapabilitiesModule;
|
|
64
81
|
retention: RetentionModule;
|
|
65
82
|
};
|
|
66
83
|
/**
|
|
67
|
-
* Human SuperAdmin surface. Every method refuses an API token
|
|
68
|
-
* request leaves the process
|
|
84
|
+
* Human SuperAdmin surface. Every method refuses an API token or an app
|
|
85
|
+
* credential before the request leaves the process
|
|
86
|
+
* (`PLATFORM_REQUIRES_SESSION`).
|
|
69
87
|
*/
|
|
70
|
-
platform: PlatformModule
|
|
88
|
+
platform: PlatformModule & {
|
|
89
|
+
apps: PlatformAppsModule;
|
|
90
|
+
};
|
|
71
91
|
/** A derived client on another tenant, sharing this one's principal. */
|
|
72
92
|
withTenant(tenant: Partial<TenantContext> & {
|
|
73
93
|
scope?: TenantScope;
|
package/dist/client.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { createChannelResolver, createTransport, mergeTenant, UNSCOPED_TENANT, ValidationError, } from '@sixtyninecommerce/core';
|
|
2
2
|
import { createApiTokensModule } from './api-tokens/index.js';
|
|
3
|
+
import { createAppCredential, } from './apps/credential.js';
|
|
4
|
+
import { createAppsModule } from './apps/index.js';
|
|
3
5
|
import { createAuditModule } from './audit/index.js';
|
|
4
6
|
import { createAuthModule } from './auth/index.js';
|
|
5
7
|
import { createFormsModule } from './content/forms.js';
|
|
6
8
|
import { createHeadsModule } from './heads/index.js';
|
|
7
9
|
import { createLocationsModule } from './locations/index.js';
|
|
8
10
|
import { createMetaModule } from './meta/index.js';
|
|
11
|
+
import { createPlatformAppsModule } from './platform/apps.js';
|
|
9
12
|
import { createPlatformModule } from './platform/provision-store.js';
|
|
10
13
|
import { createRedirectsModule } from './seo/redirects.js';
|
|
14
|
+
import { createCapabilitiesModule } from './tenant/capabilities.js';
|
|
11
15
|
import { createChannelsModule } from './tenant/channels.js';
|
|
12
16
|
import { createRetentionModule } from './tenant/retention.js';
|
|
13
17
|
/**
|
|
@@ -16,7 +20,10 @@ import { createRetentionModule } from './tenant/retention.js';
|
|
|
16
20
|
* the dashboard is — so the refusal is specifically about a long-lived token.
|
|
17
21
|
*/
|
|
18
22
|
function assertNotInBrowser(auth) {
|
|
19
|
-
|
|
23
|
+
// An app's PRIVATE KEY is the strongest credential this SDK accepts — it mints
|
|
24
|
+
// tokens rather than being one — so it is refused in a browser exactly as a
|
|
25
|
+
// long-lived token is, and for the same reason.
|
|
26
|
+
if (!('token' in auth) && !('app' in auth))
|
|
20
27
|
return;
|
|
21
28
|
// Indexed off a typed view of globalThis: this package compiles without the
|
|
22
29
|
// DOM lib on purpose, so that a stray `document.` reference cannot slip in.
|
|
@@ -24,9 +31,10 @@ function assertNotInBrowser(auth) {
|
|
|
24
31
|
const inBrowser = runtime.window !== undefined && runtime.document !== undefined;
|
|
25
32
|
if (!inBrowser)
|
|
26
33
|
return;
|
|
34
|
+
const credential = 'app' in auth ? "An app's private key" : 'An API token';
|
|
27
35
|
throw new ValidationError({
|
|
28
36
|
code: 'CREDENTIAL_IN_BROWSER',
|
|
29
|
-
message:
|
|
37
|
+
message: `${credential} must never be constructed in a browser — it would ship the credential to every visitor. Use a session bearer in the browser and keep machine credentials server-side.`,
|
|
30
38
|
status: 400,
|
|
31
39
|
service: 'vendure',
|
|
32
40
|
});
|
|
@@ -140,15 +148,101 @@ export function createAdminClient(config) {
|
|
|
140
148
|
});
|
|
141
149
|
if ('session' in config.auth)
|
|
142
150
|
assertSessionIsWritable(config.auth.session);
|
|
151
|
+
else if ('app' in config.auth)
|
|
152
|
+
assertAppCredentialIsPresent(config.auth);
|
|
143
153
|
else
|
|
144
154
|
assertTokenIsPresent(config.auth);
|
|
145
155
|
// Shared by every client `withTenant` derives, because the session store is.
|
|
146
156
|
// `rotations` lets `login()` tell a bearer THIS sign-in produced apart from
|
|
147
157
|
// one that merely sits in the slot already.
|
|
148
158
|
const shared = { rotations: 0 };
|
|
149
|
-
|
|
159
|
+
// One credential per CLIENT, shared with everything `withTenant` derives:
|
|
160
|
+
// the token is minted for an installation, and an installation is one app on
|
|
161
|
+
// one store — deriving a tenant does not change which credential it is.
|
|
162
|
+
const credential = 'app' in config.auth ? appCredentialFor(config, config.auth) : undefined;
|
|
163
|
+
return buildClient(config, config.tenant, resolver, shared, credential);
|
|
150
164
|
}
|
|
151
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Nothing about an app credential is readable live (unlike `auth.token`, which
|
|
167
|
+
* may be a getter over a secret store), so every field is checked at
|
|
168
|
+
* construction rather than on the call that would have used it.
|
|
169
|
+
*/
|
|
170
|
+
function assertAppCredentialIsPresent(auth) {
|
|
171
|
+
const missing = [
|
|
172
|
+
isPresent(auth.app?.appId) ? null : 'app.appId',
|
|
173
|
+
isPresent(auth.app?.kid) ? null : 'app.kid',
|
|
174
|
+
auth.app?.privateKey ? null : 'app.privateKey',
|
|
175
|
+
isPresent(auth.installationId) ? null : 'installationId',
|
|
176
|
+
].filter((field) => field !== null);
|
|
177
|
+
if (missing.length === 0)
|
|
178
|
+
return;
|
|
179
|
+
throw new ValidationError({
|
|
180
|
+
code: 'APP_CREDENTIAL_INCOMPLETE',
|
|
181
|
+
message: `An app credential needs ${missing.join(', ')}. Without them no assertion can be signed and every call would go out unauthenticated.`,
|
|
182
|
+
status: 400,
|
|
183
|
+
service: 'vendure',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function appCredentialFor(config, auth) {
|
|
187
|
+
// The exchange is fetched DIRECTLY, never through the wrapper below: that
|
|
188
|
+
// wrapper asks this credential for a bearer, so routing the mint through it
|
|
189
|
+
// would recurse forever on the very first call.
|
|
190
|
+
const baseFetch = config.fetch ?? globalThis.fetch;
|
|
191
|
+
return createAppCredential(auth.app, auth.installationId, config.urls.vendureUrl, baseFetch);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Every request an app makes goes out with a LIVE bearer.
|
|
195
|
+
*
|
|
196
|
+
* Done in `fetch` rather than in `principal` because minting is asynchronous and
|
|
197
|
+
* `principal` is not: a synchronous getter cannot wait for an exchange. The
|
|
198
|
+
* header the transport built is overwritten here, which is also what makes the
|
|
199
|
+
* first call work — at that point nothing has been minted and `peek()` is empty.
|
|
200
|
+
*/
|
|
201
|
+
function appFetch(credential, base) {
|
|
202
|
+
const send = async (input, init, bearer) => {
|
|
203
|
+
const headers = { ...(init?.headers ?? {}) };
|
|
204
|
+
headers.Authorization = `Bearer ${bearer}`;
|
|
205
|
+
return base(input, { ...init, headers });
|
|
206
|
+
};
|
|
207
|
+
return async (input, init) => {
|
|
208
|
+
// No try/catch: a credential failure must reach the caller AS ITSELF, and it
|
|
209
|
+
// already does. `createTransport`'s `send()` opens its catch with
|
|
210
|
+
// `if (error instanceof PlatformError) throw error`, so the refusal the
|
|
211
|
+
// exchange returned — built by `errorFromResponse`, carrying the server's
|
|
212
|
+
// own `trace_id` — passes through untouched. Catching it here to re-emit a
|
|
213
|
+
// synthesized problem+json would only lose that trace id and turn a
|
|
214
|
+
// status-0 transport failure into a 503.
|
|
215
|
+
const bearer = await credential.current();
|
|
216
|
+
const response = await send(input, init, bearer);
|
|
217
|
+
// A 401 on a token this SDK believes is live means the SERVER killed it —
|
|
218
|
+
// an app suspended and resumed, a key revoked, a tenant re-consenting — and
|
|
219
|
+
// the cached bearer can be up to an hour from its natural expiry. Without
|
|
220
|
+
// this the app is simply down for that hour: `PermissionDeniedError` is not
|
|
221
|
+
// retryable, and nothing else ever calls `invalidate()`. Exactly ONE retry,
|
|
222
|
+
// and only when a fresh bearer actually differs, so a genuinely
|
|
223
|
+
// unauthorised call cannot become an exchange loop.
|
|
224
|
+
if (response.status !== 401)
|
|
225
|
+
return response;
|
|
226
|
+
// The 401 body is read and discarded: an unread body holds its socket out of
|
|
227
|
+
// the keep-alive pool, and this one is replaced by the retry's.
|
|
228
|
+
void response
|
|
229
|
+
.clone()
|
|
230
|
+
.text()
|
|
231
|
+
.catch(() => undefined);
|
|
232
|
+
// `refresh`, not invalidate-then-current: it returns a bearer a sibling has
|
|
233
|
+
// already minted, and joins a refresh already running for this same dead
|
|
234
|
+
// one, so a burst of concurrent 401s costs ONE exchange rather than one per
|
|
235
|
+
// request.
|
|
236
|
+
const fresh = await credential.refresh(bearer);
|
|
237
|
+
// Compared against the bearer THIS request used: an identical token back
|
|
238
|
+
// from the exchange (a caching proxy in front of it) must end here rather
|
|
239
|
+
// than retry forever.
|
|
240
|
+
if (fresh === bearer)
|
|
241
|
+
return response;
|
|
242
|
+
return send(input, init, fresh);
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function buildClient(config, overrides, resolver, shared, credential) {
|
|
152
246
|
// No scope configured leaves the client on UNSCOPED_TENANT, which only the
|
|
153
247
|
// auth documents may use; the transport refuses everything else with
|
|
154
248
|
// TENANT_REQUIRED (spec §15.2).
|
|
@@ -160,6 +254,12 @@ function buildClient(config, overrides, resolver, shared) {
|
|
|
160
254
|
channels: resolver,
|
|
161
255
|
tenant,
|
|
162
256
|
principal: () => {
|
|
257
|
+
// The bearer the LAST mint produced, which `appFetch` overwrites with a
|
|
258
|
+
// live one before the request leaves. Empty on the very first call, and
|
|
259
|
+
// deliberately not refused for it: an app credential is complete or it
|
|
260
|
+
// was rejected at construction.
|
|
261
|
+
if (credential)
|
|
262
|
+
return { kind: 'bearer', token: credential.peek() };
|
|
163
263
|
if (!store) {
|
|
164
264
|
// Read live, so a getter over a request context follows the request
|
|
165
265
|
// being served — and refuse an empty one HERE rather than sending an
|
|
@@ -191,7 +291,12 @@ function buildClient(config, overrides, resolver, shared) {
|
|
|
191
291
|
},
|
|
192
292
|
};
|
|
193
293
|
},
|
|
194
|
-
|
|
294
|
+
// Every app request carries a freshly minted bearer; see `appFetch`.
|
|
295
|
+
...(credential
|
|
296
|
+
? { fetch: appFetch(credential, config.fetch ?? globalThis.fetch) }
|
|
297
|
+
: config.fetch
|
|
298
|
+
? { fetch: config.fetch }
|
|
299
|
+
: {}),
|
|
195
300
|
...(config.retry ? { retry: config.retry } : {}),
|
|
196
301
|
...(config.timeoutMs === undefined ? {} : { timeoutMs: config.timeoutMs }),
|
|
197
302
|
...(config.onDeprecation ? { onDeprecation: config.onDeprecation } : {}),
|
|
@@ -219,6 +324,7 @@ function buildClient(config, overrides, resolver, shared) {
|
|
|
219
324
|
},
|
|
220
325
|
}),
|
|
221
326
|
apiTokens: createApiTokensModule(transport),
|
|
327
|
+
apps: createAppsModule(transport),
|
|
222
328
|
audit: createAuditModule(transport),
|
|
223
329
|
channels: createChannelsModule(resolver),
|
|
224
330
|
content: { forms: createFormsModule(transport) },
|
|
@@ -226,8 +332,14 @@ function buildClient(config, overrides, resolver, shared) {
|
|
|
226
332
|
locations: createLocationsModule(transport),
|
|
227
333
|
meta: createMetaModule(transport),
|
|
228
334
|
seo: { redirects: createRedirectsModule(transport) },
|
|
229
|
-
tenant: {
|
|
230
|
-
|
|
335
|
+
tenant: {
|
|
336
|
+
capabilities: createCapabilitiesModule(transport),
|
|
337
|
+
retention: createRetentionModule(transport),
|
|
338
|
+
},
|
|
339
|
+
platform: {
|
|
340
|
+
...createPlatformModule(transport, config),
|
|
341
|
+
apps: createPlatformAppsModule(transport, config),
|
|
342
|
+
},
|
|
231
343
|
withTenant(next) {
|
|
232
344
|
// `mergeTenant`, not a spread: it merges `actor` FIELD BY FIELD, so a
|
|
233
345
|
// derived client that names only a registerRef keeps the staffRef this
|
|
@@ -238,7 +350,7 @@ function buildClient(config, overrides, resolver, shared) {
|
|
|
238
350
|
// Resolver and rotation count are shared, and the session store IS the
|
|
239
351
|
// caller's object — so a derived client reuses the channel cache and the
|
|
240
352
|
// bearer this one signed in with, in both directions and in either order.
|
|
241
|
-
return buildClient(config, mergeTenant(tenant, next), resolver, shared);
|
|
353
|
+
return buildClient(config, mergeTenant(tenant, next), resolver, shared, credential);
|
|
242
354
|
},
|
|
243
355
|
get rateLimit() {
|
|
244
356
|
return transport.rateLimit;
|
package/dist/index.d.ts
CHANGED
|
@@ -25,19 +25,24 @@ 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';
|
|
33
36
|
export type { CreateHeadInput, Head, HeadsModule, UpdateHeadInput, } from './heads/index.js';
|
|
34
37
|
export type { LocationProfile, LocationStockLocation, LocationsModule, OpeningHours, OpeningHoursDay, OpeningHoursInterval, StockLocationAddress, StockLocationAddressInput, UpsertLocationProfileInput, } from './locations/index.js';
|
|
35
38
|
export { OPENING_HOURS_DAYS } from './locations/index.js';
|
|
36
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';
|
|
37
41
|
export { requireHumanSession } from './platform/guard.js';
|
|
38
42
|
export type { JobHandle, PlatformModule, ProvisionProfile, ProvisionRun, ProvisionRunStatus, ProvisionStep, ProvisionStepStatus, ProvisionStoreInput, ProvisionStoreResult, WaitOptions, } from './platform/provision-store.js';
|
|
39
43
|
export { PROVISION_PROFILES } from './platform/provision-store.js';
|
|
40
44
|
export type { Redirect, RedirectCreateInput, RedirectsModule, RedirectUpdateInput, } from './seo/redirects.js';
|
|
45
|
+
export type { CapabilitiesModule, Capability, CapabilityManifest, } from './tenant/capabilities.js';
|
|
41
46
|
export type { ChannelsModule } from './tenant/channels.js';
|
|
42
47
|
export type { ChannelRetention, RetentionHold, RetentionModule, SetRetentionInput, } from './tenant/retention.js';
|
|
43
48
|
export type { ApiToken, Call, ChannelInfo, CreateApiTokenInput, ListOptions, Me, TenantContext, TenantScope, UpdateApiTokenInput, WhoAmI, } from './types.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;
|