@fntio/infra-client-plugin-sdk 0.0.0-stage → 0.2.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/LICENSE +93 -0
- package/README.md +279 -2
- package/dist/backend/context.d.ts +31 -0
- package/dist/backend/context.js +58 -0
- package/dist/backend/index.d.ts +9 -0
- package/dist/backend/index.js +26 -0
- package/dist/browser/client.d.ts +59 -0
- package/dist/browser/client.js +288 -0
- package/dist/browser/errors.d.ts +14 -0
- package/dist/browser/errors.js +32 -0
- package/dist/browser/http.d.ts +72 -0
- package/dist/browser/http.js +170 -0
- package/dist/browser/index.d.ts +11 -0
- package/dist/browser/index.js +28 -0
- package/dist/browser/theme-apply.d.ts +22 -0
- package/dist/browser/theme-apply.js +22 -0
- package/dist/contracts/access.d.ts +14 -0
- package/dist/contracts/access.js +41 -0
- package/dist/contracts/authorizer.d.ts +17 -0
- package/dist/contracts/authorizer.js +44 -0
- package/dist/contracts/canonical.d.ts +2 -0
- package/dist/contracts/canonical.js +21 -0
- package/dist/contracts/catalog.d.ts +37 -0
- package/dist/contracts/catalog.js +18 -0
- package/dist/contracts/discovery.d.ts +78 -0
- package/dist/contracts/discovery.js +48 -0
- package/dist/contracts/fields.d.ts +7 -0
- package/dist/contracts/fields.js +2 -0
- package/dist/contracts/manifest.d.ts +59 -0
- package/dist/contracts/manifest.js +19 -0
- package/dist/contracts/messages.d.ts +108 -0
- package/dist/contracts/messages.js +103 -0
- package/dist/contracts/paths.d.ts +20 -0
- package/dist/contracts/paths.js +49 -0
- package/dist/contracts/theme.d.ts +21 -0
- package/dist/contracts/theme.js +60 -0
- package/dist/contracts/transport.d.ts +14 -0
- package/dist/contracts/transport.js +10 -0
- package/dist/contracts/validate.d.ts +27 -0
- package/dist/contracts/validate.js +321 -0
- package/dist/fixtures/devHost.d.ts +57 -0
- package/dist/fixtures/devHost.js +91 -0
- package/dist/fixtures/fakeDocument.d.ts +12 -0
- package/dist/fixtures/fakeDocument.js +22 -0
- package/dist/fixtures/fakeWindows.d.ts +22 -0
- package/dist/fixtures/fakeWindows.js +46 -0
- package/dist/fixtures/index.d.ts +4 -0
- package/dist/fixtures/index.js +20 -0
- package/dist/fixtures/samples.d.ts +37 -0
- package/dist/fixtures/samples.js +220 -0
- package/dist/host/host.d.ts +47 -0
- package/dist/host/host.js +117 -0
- package/dist/host/index.d.ts +5 -0
- package/dist/host/index.js +22 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +28 -0
- package/package.json +97 -4
- package/skills/build-infra-client-plugin/SKILL.md +61 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PluginPermission } from "./manifest";
|
|
2
|
+
export declare const PLUGIN_TARGET_PREFIX = "plugin#";
|
|
3
|
+
/** The Access-table target: `plugin#<pluginId>#<accountNumber>#<user|admin>`. */
|
|
4
|
+
export interface PluginGrantTarget {
|
|
5
|
+
readonly pluginId: string;
|
|
6
|
+
readonly accountNumber: string;
|
|
7
|
+
readonly permission: PluginPermission;
|
|
8
|
+
}
|
|
9
|
+
export declare function pluginTargetKey(target: PluginGrantTarget): string;
|
|
10
|
+
export declare function parsePluginTargetKey(key: string): PluginGrantTarget | null;
|
|
11
|
+
/** Fixed platform rule: admin includes user, but only when the plugin declares user. */
|
|
12
|
+
export declare function pluginPermissionsImpliedBy(granted: PluginPermission, declared: readonly PluginPermission[]): PluginPermission[];
|
|
13
|
+
/** Effective capabilities from a set of held (stored) grants for one plugin/account. */
|
|
14
|
+
export declare function pluginEffectivePermissions(held: readonly PluginPermission[], declared: readonly PluginPermission[]): PluginPermission[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PLUGIN_TARGET_PREFIX = void 0;
|
|
4
|
+
exports.pluginTargetKey = pluginTargetKey;
|
|
5
|
+
exports.parsePluginTargetKey = parsePluginTargetKey;
|
|
6
|
+
exports.pluginPermissionsImpliedBy = pluginPermissionsImpliedBy;
|
|
7
|
+
exports.pluginEffectivePermissions = pluginEffectivePermissions;
|
|
8
|
+
const manifest_1 = require("./manifest");
|
|
9
|
+
exports.PLUGIN_TARGET_PREFIX = "plugin#";
|
|
10
|
+
function pluginTargetKey(target) {
|
|
11
|
+
return `${exports.PLUGIN_TARGET_PREFIX}${target.pluginId}#${target.accountNumber}#${target.permission}`;
|
|
12
|
+
}
|
|
13
|
+
function parsePluginTargetKey(key) {
|
|
14
|
+
if (!key.startsWith(exports.PLUGIN_TARGET_PREFIX))
|
|
15
|
+
return null;
|
|
16
|
+
const parts = key.slice(exports.PLUGIN_TARGET_PREFIX.length).split("#");
|
|
17
|
+
if (parts.length !== 3)
|
|
18
|
+
return null;
|
|
19
|
+
const [pluginId, accountNumber, permission] = parts;
|
|
20
|
+
if (!manifest_1.PLUGIN_ID_PATTERN.test(pluginId) || !manifest_1.AWS_ACCOUNT_PATTERN.test(accountNumber))
|
|
21
|
+
return null;
|
|
22
|
+
if (!manifest_1.PLUGIN_PERMISSIONS.includes(permission))
|
|
23
|
+
return null;
|
|
24
|
+
return { pluginId, accountNumber, permission: permission };
|
|
25
|
+
}
|
|
26
|
+
/** Fixed platform rule: admin includes user, but only when the plugin declares user. */
|
|
27
|
+
function pluginPermissionsImpliedBy(granted, declared) {
|
|
28
|
+
if (!declared.includes(granted))
|
|
29
|
+
return [];
|
|
30
|
+
if (granted === "admin" && declared.includes("user"))
|
|
31
|
+
return ["admin", "user"];
|
|
32
|
+
return [granted];
|
|
33
|
+
}
|
|
34
|
+
/** Effective capabilities from a set of held (stored) grants for one plugin/account. */
|
|
35
|
+
function pluginEffectivePermissions(held, declared) {
|
|
36
|
+
const out = new Set();
|
|
37
|
+
for (const h of held)
|
|
38
|
+
for (const p of pluginPermissionsImpliedBy(h, declared))
|
|
39
|
+
out.add(p);
|
|
40
|
+
return manifest_1.PLUGIN_PERMISSIONS.filter((p) => out.has(p));
|
|
41
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { PluginPermission } from "./manifest";
|
|
2
|
+
export type PluginPrincipalType = "user" | "service";
|
|
3
|
+
/** What the shared authorizer attaches to every allowed request; scalars only, never a token. */
|
|
4
|
+
export interface PluginAuthorizerContextV1 {
|
|
5
|
+
readonly pluginId: string;
|
|
6
|
+
readonly pluginRevision: string;
|
|
7
|
+
readonly routeId: string;
|
|
8
|
+
readonly accountNumber: string;
|
|
9
|
+
readonly requiredPermission: PluginPermission;
|
|
10
|
+
readonly subject: string;
|
|
11
|
+
readonly principalType: PluginPrincipalType;
|
|
12
|
+
readonly correlationId: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const PLUGIN_AUTHORIZER_CONTEXT_KEYS: readonly ["pluginId", "pluginRevision", "routeId", "accountNumber", "requiredPermission", "subject", "principalType", "correlationId"];
|
|
15
|
+
/** Fail closed: a handler must not run on a context it cannot read completely. */
|
|
16
|
+
export declare function parsePluginAuthorizerContext(raw: unknown): PluginAuthorizerContextV1;
|
|
17
|
+
export declare function tryParsePluginAuthorizerContext(raw: unknown): PluginAuthorizerContextV1 | null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PLUGIN_AUTHORIZER_CONTEXT_KEYS = void 0;
|
|
4
|
+
exports.parsePluginAuthorizerContext = parsePluginAuthorizerContext;
|
|
5
|
+
exports.tryParsePluginAuthorizerContext = tryParsePluginAuthorizerContext;
|
|
6
|
+
const manifest_1 = require("./manifest");
|
|
7
|
+
exports.PLUGIN_AUTHORIZER_CONTEXT_KEYS = [
|
|
8
|
+
"pluginId", "pluginRevision", "routeId", "accountNumber",
|
|
9
|
+
"requiredPermission", "subject", "principalType", "correlationId",
|
|
10
|
+
];
|
|
11
|
+
/** Fail closed: a handler must not run on a context it cannot read completely. */
|
|
12
|
+
function parsePluginAuthorizerContext(raw) {
|
|
13
|
+
const ctx = tryParsePluginAuthorizerContext(raw);
|
|
14
|
+
if (!ctx)
|
|
15
|
+
throw new Error("plugin authorizer context is missing or malformed");
|
|
16
|
+
return ctx;
|
|
17
|
+
}
|
|
18
|
+
function tryParsePluginAuthorizerContext(raw) {
|
|
19
|
+
if (!raw || typeof raw !== "object")
|
|
20
|
+
return null;
|
|
21
|
+
const r = raw;
|
|
22
|
+
for (const key of exports.PLUGIN_AUTHORIZER_CONTEXT_KEYS) {
|
|
23
|
+
if (typeof r[key] !== "string" || r[key].length === 0)
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (!manifest_1.PLUGIN_ID_PATTERN.test(r.pluginId))
|
|
27
|
+
return null;
|
|
28
|
+
if (!manifest_1.AWS_ACCOUNT_PATTERN.test(r.accountNumber))
|
|
29
|
+
return null;
|
|
30
|
+
if (!manifest_1.PLUGIN_PERMISSIONS.includes(r.requiredPermission))
|
|
31
|
+
return null;
|
|
32
|
+
if (r.principalType !== "user" && r.principalType !== "service")
|
|
33
|
+
return null;
|
|
34
|
+
return {
|
|
35
|
+
pluginId: r.pluginId,
|
|
36
|
+
pluginRevision: r.pluginRevision,
|
|
37
|
+
routeId: r.routeId,
|
|
38
|
+
accountNumber: r.accountNumber,
|
|
39
|
+
requiredPermission: r.requiredPermission,
|
|
40
|
+
subject: r.subject,
|
|
41
|
+
principalType: r.principalType,
|
|
42
|
+
correlationId: r.correlationId,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.canonicalJson = canonicalJson;
|
|
4
|
+
/** JSON with object keys sorted at every level, so equal manifests hash and compare equal. */
|
|
5
|
+
function canonicalJson(value) {
|
|
6
|
+
return JSON.stringify(sortKeys(value));
|
|
7
|
+
}
|
|
8
|
+
function sortKeys(value) {
|
|
9
|
+
if (Array.isArray(value))
|
|
10
|
+
return value.map(sortKeys);
|
|
11
|
+
if (value && typeof value === "object") {
|
|
12
|
+
const out = {};
|
|
13
|
+
for (const key of Object.keys(value).sort()) {
|
|
14
|
+
const v = value[key];
|
|
15
|
+
if (v !== undefined)
|
|
16
|
+
out[key] = sortKeys(v);
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { PluginManifestV1, PluginRoute } from "./manifest";
|
|
2
|
+
export interface PluginCatalogUiEntryV1 {
|
|
3
|
+
readonly label: string;
|
|
4
|
+
readonly icon?: string;
|
|
5
|
+
readonly order?: number;
|
|
6
|
+
/** Absolute path on the portal origin, under the immutable asset prefix. */
|
|
7
|
+
readonly url: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PluginCatalogRouteV1 extends PluginRoute {
|
|
10
|
+
/** The gateway resource template the authorizer resolves the route by. */
|
|
11
|
+
readonly resourcePath: string;
|
|
12
|
+
}
|
|
13
|
+
/** One PluginCatalog item, keyed by `pluginId`. Deployment-written; holds no user grants. */
|
|
14
|
+
export interface PluginCatalogRecordV1 {
|
|
15
|
+
readonly pluginId: string;
|
|
16
|
+
readonly schemaVersion: number;
|
|
17
|
+
readonly manifest: PluginManifestV1;
|
|
18
|
+
/** Content digest of permission/route metadata plus the asset hash. */
|
|
19
|
+
readonly revision: string;
|
|
20
|
+
readonly assetPrefix?: string;
|
|
21
|
+
/** false = tombstone: the definition was removed; labels are kept for historical grants. */
|
|
22
|
+
readonly installed: boolean;
|
|
23
|
+
/** The deployment that owns this record; another owner claiming the id is a deploy error. */
|
|
24
|
+
readonly owner: string;
|
|
25
|
+
readonly ui?: {
|
|
26
|
+
readonly user?: PluginCatalogUiEntryV1;
|
|
27
|
+
readonly admin?: PluginCatalogUiEntryV1;
|
|
28
|
+
};
|
|
29
|
+
readonly routes: readonly PluginCatalogRouteV1[];
|
|
30
|
+
readonly installedAt: string;
|
|
31
|
+
readonly updatedAt: string;
|
|
32
|
+
readonly removedAt?: string;
|
|
33
|
+
}
|
|
34
|
+
export type PluginCatalogStatus = "active" | "disabled" | "removed" | "unsupported";
|
|
35
|
+
export declare function pluginCatalogStatus(record: PluginCatalogRecordV1): PluginCatalogStatus;
|
|
36
|
+
/** Only an active record may authorize requests or appear in caller discovery. */
|
|
37
|
+
export declare function isPluginActive(record: PluginCatalogRecordV1): boolean;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pluginCatalogStatus = pluginCatalogStatus;
|
|
4
|
+
exports.isPluginActive = isPluginActive;
|
|
5
|
+
const manifest_1 = require("./manifest");
|
|
6
|
+
function pluginCatalogStatus(record) {
|
|
7
|
+
if (!record.installed)
|
|
8
|
+
return "removed";
|
|
9
|
+
if (record.schemaVersion !== manifest_1.PLUGIN_MANIFEST_SCHEMA_VERSION ||
|
|
10
|
+
record.manifest.hostApiVersion !== manifest_1.PLUGIN_HOST_API_VERSION) {
|
|
11
|
+
return "unsupported";
|
|
12
|
+
}
|
|
13
|
+
return record.manifest.enabled ? "active" : "disabled";
|
|
14
|
+
}
|
|
15
|
+
/** Only an active record may authorize requests or appear in caller discovery. */
|
|
16
|
+
function isPluginActive(record) {
|
|
17
|
+
return pluginCatalogStatus(record) === "active";
|
|
18
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { PluginCatalogStatus } from "./catalog";
|
|
2
|
+
import { PluginPermission, PluginRoute } from "./manifest";
|
|
3
|
+
export declare const PLUGIN_DISCOVERY_DEFAULT_REFRESH_SECONDS = 60;
|
|
4
|
+
export declare const PLUGIN_DISCOVERY_MIN_REFRESH_SECONDS = 15;
|
|
5
|
+
export declare const PLUGIN_DISCOVERY_MAX_REFRESH_SECONDS = 300;
|
|
6
|
+
export interface PluginDiscoveryUiEntryV1 {
|
|
7
|
+
readonly label: string;
|
|
8
|
+
readonly icon?: string;
|
|
9
|
+
readonly order?: number;
|
|
10
|
+
readonly url: string;
|
|
11
|
+
}
|
|
12
|
+
export interface PluginDiscoveryAccountV1 {
|
|
13
|
+
readonly accountNumber: string;
|
|
14
|
+
readonly displayName: string;
|
|
15
|
+
readonly regions?: readonly string[];
|
|
16
|
+
/** Effective capabilities, implication applied; not stored assignments. */
|
|
17
|
+
readonly permissions: readonly PluginPermission[];
|
|
18
|
+
}
|
|
19
|
+
export interface PluginDiscoveryEntryV1 {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly displayName: string;
|
|
22
|
+
readonly description?: string;
|
|
23
|
+
readonly version: string;
|
|
24
|
+
readonly revision: string;
|
|
25
|
+
readonly apiBaseUrl: string;
|
|
26
|
+
readonly ui: {
|
|
27
|
+
readonly user?: PluginDiscoveryUiEntryV1;
|
|
28
|
+
readonly admin?: PluginDiscoveryUiEntryV1;
|
|
29
|
+
};
|
|
30
|
+
readonly accounts: readonly PluginDiscoveryAccountV1[];
|
|
31
|
+
}
|
|
32
|
+
/** `GET /api/plugins`: only what the caller may use. */
|
|
33
|
+
export interface PluginDiscoveryResponseV1 {
|
|
34
|
+
readonly schemaVersion: 1;
|
|
35
|
+
readonly refreshAfterSeconds: number;
|
|
36
|
+
readonly plugins: readonly PluginDiscoveryEntryV1[];
|
|
37
|
+
}
|
|
38
|
+
export interface PluginAdminAccountV1 {
|
|
39
|
+
readonly accountNumber: string;
|
|
40
|
+
readonly displayName?: string;
|
|
41
|
+
readonly regions?: readonly string[];
|
|
42
|
+
/** false when the bound account is not registered in the Accounts catalog. */
|
|
43
|
+
readonly available: boolean;
|
|
44
|
+
readonly reason?: "unregistered";
|
|
45
|
+
}
|
|
46
|
+
export interface PluginAdminPermissionV1 {
|
|
47
|
+
readonly id: PluginPermission;
|
|
48
|
+
readonly label: string;
|
|
49
|
+
readonly description?: string;
|
|
50
|
+
readonly implies: readonly PluginPermission[];
|
|
51
|
+
}
|
|
52
|
+
export interface PluginAdminEntryV1 {
|
|
53
|
+
readonly id: string;
|
|
54
|
+
readonly displayName: string;
|
|
55
|
+
readonly description?: string;
|
|
56
|
+
readonly version: string;
|
|
57
|
+
readonly revision: string;
|
|
58
|
+
readonly schemaVersion: number;
|
|
59
|
+
readonly hostApiVersion: number;
|
|
60
|
+
readonly status: PluginCatalogStatus;
|
|
61
|
+
readonly enabled: boolean;
|
|
62
|
+
readonly installed: boolean;
|
|
63
|
+
readonly permissions: readonly PluginAdminPermissionV1[];
|
|
64
|
+
readonly accounts: readonly PluginAdminAccountV1[];
|
|
65
|
+
readonly ui: {
|
|
66
|
+
readonly user?: Omit<PluginDiscoveryUiEntryV1, "url">;
|
|
67
|
+
readonly admin?: Omit<PluginDiscoveryUiEntryV1, "url">;
|
|
68
|
+
};
|
|
69
|
+
readonly routes: readonly Pick<PluginRoute, "id" | "method" | "path" | "permission">[];
|
|
70
|
+
readonly updatedAt: string;
|
|
71
|
+
}
|
|
72
|
+
/** `GET /api/admin/plugins`: every definition, for the grant editor. */
|
|
73
|
+
export interface PluginAdminCatalogResponseV1 {
|
|
74
|
+
readonly schemaVersion: 1;
|
|
75
|
+
readonly plugins: readonly PluginAdminEntryV1[];
|
|
76
|
+
}
|
|
77
|
+
export declare function isPluginDiscoveryAccount(v: unknown): v is PluginDiscoveryAccountV1;
|
|
78
|
+
export declare function isPluginDiscoveryResponse(value: unknown): value is PluginDiscoveryResponseV1;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PLUGIN_DISCOVERY_MAX_REFRESH_SECONDS = exports.PLUGIN_DISCOVERY_MIN_REFRESH_SECONDS = exports.PLUGIN_DISCOVERY_DEFAULT_REFRESH_SECONDS = void 0;
|
|
4
|
+
exports.isPluginDiscoveryAccount = isPluginDiscoveryAccount;
|
|
5
|
+
exports.isPluginDiscoveryResponse = isPluginDiscoveryResponse;
|
|
6
|
+
const manifest_1 = require("./manifest");
|
|
7
|
+
exports.PLUGIN_DISCOVERY_DEFAULT_REFRESH_SECONDS = 60;
|
|
8
|
+
exports.PLUGIN_DISCOVERY_MIN_REFRESH_SECONDS = 15;
|
|
9
|
+
exports.PLUGIN_DISCOVERY_MAX_REFRESH_SECONDS = 300;
|
|
10
|
+
function isUiEntry(v) {
|
|
11
|
+
if (!v || typeof v !== "object")
|
|
12
|
+
return false;
|
|
13
|
+
const e = v;
|
|
14
|
+
return typeof e.label === "string" && typeof e.url === "string";
|
|
15
|
+
}
|
|
16
|
+
function isPluginDiscoveryAccount(v) {
|
|
17
|
+
if (!v || typeof v !== "object")
|
|
18
|
+
return false;
|
|
19
|
+
const a = v;
|
|
20
|
+
return (typeof a.accountNumber === "string" &&
|
|
21
|
+
manifest_1.AWS_ACCOUNT_PATTERN.test(a.accountNumber) &&
|
|
22
|
+
typeof a.displayName === "string" &&
|
|
23
|
+
(a.regions === undefined || (Array.isArray(a.regions) && a.regions.every((r) => typeof r === "string"))) &&
|
|
24
|
+
Array.isArray(a.permissions) &&
|
|
25
|
+
a.permissions.every((p) => manifest_1.PLUGIN_PERMISSIONS.includes(p)));
|
|
26
|
+
}
|
|
27
|
+
function isPluginDiscoveryResponse(value) {
|
|
28
|
+
if (!value || typeof value !== "object")
|
|
29
|
+
return false;
|
|
30
|
+
const r = value;
|
|
31
|
+
if (r.schemaVersion !== 1 || typeof r.refreshAfterSeconds !== "number" || !Array.isArray(r.plugins))
|
|
32
|
+
return false;
|
|
33
|
+
return r.plugins.every((p) => {
|
|
34
|
+
if (!p || typeof p !== "object")
|
|
35
|
+
return false;
|
|
36
|
+
const e = p;
|
|
37
|
+
const ui = e.ui;
|
|
38
|
+
return (typeof e.id === "string" &&
|
|
39
|
+
typeof e.displayName === "string" &&
|
|
40
|
+
typeof e.version === "string" &&
|
|
41
|
+
typeof e.revision === "string" &&
|
|
42
|
+
typeof e.apiBaseUrl === "string" &&
|
|
43
|
+
!!ui && typeof ui === "object" &&
|
|
44
|
+
(ui.user === undefined || isUiEntry(ui.user)) &&
|
|
45
|
+
(ui.admin === undefined || isUiEntry(ui.admin)) &&
|
|
46
|
+
Array.isArray(e.accounts) && e.accounts.every(isPluginDiscoveryAccount));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** The fields a validator reads off an unvalidated value, each still `unknown`. A finite key set rather
|
|
2
|
+
* than `Record<string, unknown>`: an index signature makes property access illegal for consumers compiling
|
|
3
|
+
* with `noPropertyAccessFromIndexSignature`, and the portal frontend compiles this source directly.
|
|
4
|
+
* Its own leaf module, so naming it drags nothing else into a consumer's bundle. */
|
|
5
|
+
export type ValidatedFields<K extends string> = {
|
|
6
|
+
[P in K]?: unknown;
|
|
7
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type PluginPermission = "user" | "admin";
|
|
2
|
+
export declare const PLUGIN_PERMISSIONS: readonly PluginPermission[];
|
|
3
|
+
export type PluginRouteMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
|
|
4
|
+
export declare const PLUGIN_ROUTE_METHODS: readonly PluginRouteMethod[];
|
|
5
|
+
export declare const PLUGIN_MANIFEST_SCHEMA_VERSION = 1;
|
|
6
|
+
export declare const PLUGIN_HOST_API_VERSION = 1;
|
|
7
|
+
/** The platform's own path parameter on every plugin route; routes may not declare it. */
|
|
8
|
+
export declare const PLUGIN_ACCOUNT_PARAM = "accountNumber";
|
|
9
|
+
export declare const PLUGIN_ID_PATTERN: RegExp;
|
|
10
|
+
/** Becomes part of the immutable asset URL, so it is limited to URL-safe characters. */
|
|
11
|
+
export declare const PLUGIN_VERSION_PATTERN: RegExp;
|
|
12
|
+
export declare const AWS_ACCOUNT_PATTERN: RegExp;
|
|
13
|
+
export declare const AWS_REGION_PATTERN: RegExp;
|
|
14
|
+
export declare const PLUGIN_ICON_PATTERN: RegExp;
|
|
15
|
+
export declare const PLUGIN_DISPLAY_NAME_MAX = 80;
|
|
16
|
+
export declare const PLUGIN_DESCRIPTION_MAX = 500;
|
|
17
|
+
/** The catalog record stores the manifest plus resolved routes and URLs; keep it well under DynamoDB's 400 KB item limit. */
|
|
18
|
+
export declare const PLUGIN_MANIFEST_MAX_BYTES = 100000;
|
|
19
|
+
export interface PluginAccountBinding {
|
|
20
|
+
readonly accountNumber: string;
|
|
21
|
+
/** Constrains the account context handed to the plugin; not a grant dimension. */
|
|
22
|
+
readonly regions?: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
export interface PluginPermissionDefinition {
|
|
25
|
+
readonly id: PluginPermission;
|
|
26
|
+
readonly label: string;
|
|
27
|
+
readonly description?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface PluginUiEntry {
|
|
30
|
+
readonly label: string;
|
|
31
|
+
readonly icon?: string;
|
|
32
|
+
readonly order?: number;
|
|
33
|
+
/** A file inside the asset directory, optionally with a hash route: "index.html#/user". */
|
|
34
|
+
readonly entry: string;
|
|
35
|
+
}
|
|
36
|
+
export interface PluginRoute {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly method: PluginRouteMethod;
|
|
39
|
+
/** Relative to the account-scoped plugin root; literal segments and single-segment `{params}` only. */
|
|
40
|
+
readonly path: string;
|
|
41
|
+
readonly permission: PluginPermission;
|
|
42
|
+
readonly handlerKey: string;
|
|
43
|
+
}
|
|
44
|
+
export interface PluginManifestV1 {
|
|
45
|
+
readonly schemaVersion: 1;
|
|
46
|
+
readonly id: string;
|
|
47
|
+
readonly displayName: string;
|
|
48
|
+
readonly description?: string;
|
|
49
|
+
readonly version: string;
|
|
50
|
+
readonly hostApiVersion: 1;
|
|
51
|
+
readonly enabled: boolean;
|
|
52
|
+
readonly accounts: readonly PluginAccountBinding[];
|
|
53
|
+
readonly permissions: readonly PluginPermissionDefinition[];
|
|
54
|
+
readonly ui?: {
|
|
55
|
+
readonly user?: PluginUiEntry;
|
|
56
|
+
readonly admin?: PluginUiEntry;
|
|
57
|
+
};
|
|
58
|
+
readonly routes: readonly PluginRoute[];
|
|
59
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PLUGIN_MANIFEST_MAX_BYTES = exports.PLUGIN_DESCRIPTION_MAX = exports.PLUGIN_DISPLAY_NAME_MAX = exports.PLUGIN_ICON_PATTERN = exports.AWS_REGION_PATTERN = exports.AWS_ACCOUNT_PATTERN = exports.PLUGIN_VERSION_PATTERN = exports.PLUGIN_ID_PATTERN = exports.PLUGIN_ACCOUNT_PARAM = exports.PLUGIN_HOST_API_VERSION = exports.PLUGIN_MANIFEST_SCHEMA_VERSION = exports.PLUGIN_ROUTE_METHODS = exports.PLUGIN_PERMISSIONS = void 0;
|
|
4
|
+
exports.PLUGIN_PERMISSIONS = ["user", "admin"];
|
|
5
|
+
exports.PLUGIN_ROUTE_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"];
|
|
6
|
+
exports.PLUGIN_MANIFEST_SCHEMA_VERSION = 1;
|
|
7
|
+
exports.PLUGIN_HOST_API_VERSION = 1;
|
|
8
|
+
/** The platform's own path parameter on every plugin route; routes may not declare it. */
|
|
9
|
+
exports.PLUGIN_ACCOUNT_PARAM = "accountNumber";
|
|
10
|
+
exports.PLUGIN_ID_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
|
|
11
|
+
/** Becomes part of the immutable asset URL, so it is limited to URL-safe characters. */
|
|
12
|
+
exports.PLUGIN_VERSION_PATTERN = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$/;
|
|
13
|
+
exports.AWS_ACCOUNT_PATTERN = /^\d{12}$/;
|
|
14
|
+
exports.AWS_REGION_PATTERN = /^[a-z]{2}(-[a-z]+)+-\d$/;
|
|
15
|
+
exports.PLUGIN_ICON_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
16
|
+
exports.PLUGIN_DISPLAY_NAME_MAX = 80;
|
|
17
|
+
exports.PLUGIN_DESCRIPTION_MAX = 500;
|
|
18
|
+
/** The catalog record stores the manifest plus resolved routes and URLs; keep it well under DynamoDB's 400 KB item limit. */
|
|
19
|
+
exports.PLUGIN_MANIFEST_MAX_BYTES = 100_000;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { PluginPermission } from "./manifest";
|
|
2
|
+
import { PluginPrincipalType } from "./authorizer";
|
|
3
|
+
import { PluginTheme } from "./theme";
|
|
4
|
+
export declare const PLUGIN_PROTOCOL = "infra-client-plugin";
|
|
5
|
+
export declare const PLUGIN_PROTOCOL_VERSION = 2;
|
|
6
|
+
export declare const PLUGIN_HANDSHAKE_TIMEOUT_MS = 10000;
|
|
7
|
+
/** The current API bearer token and its expiry (epoch milliseconds). The host never SENDS the refresh token
|
|
8
|
+
* (the frame is same-origin and could read the portal's storage; see README "Trust model"). */
|
|
9
|
+
export interface PluginSession {
|
|
10
|
+
readonly token: string;
|
|
11
|
+
readonly expiresAt: number;
|
|
12
|
+
}
|
|
13
|
+
export interface PluginUserContext {
|
|
14
|
+
readonly subject: string;
|
|
15
|
+
readonly principalType: PluginPrincipalType;
|
|
16
|
+
readonly email?: string;
|
|
17
|
+
readonly displayName?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface PluginAccountContext {
|
|
20
|
+
readonly accountNumber: string;
|
|
21
|
+
readonly displayName: string;
|
|
22
|
+
readonly regions?: readonly string[];
|
|
23
|
+
readonly permissions: readonly PluginPermission[];
|
|
24
|
+
}
|
|
25
|
+
export interface PluginInfoContext {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly displayName: string;
|
|
28
|
+
readonly version: string;
|
|
29
|
+
readonly revision: string;
|
|
30
|
+
readonly apiBaseUrl: string;
|
|
31
|
+
}
|
|
32
|
+
export type PluginEntryKind = PluginPermission;
|
|
33
|
+
export interface HostToPluginPayloads {
|
|
34
|
+
init: {
|
|
35
|
+
readonly session: PluginSession;
|
|
36
|
+
readonly user: PluginUserContext;
|
|
37
|
+
readonly plugin: PluginInfoContext;
|
|
38
|
+
readonly entry: PluginEntryKind;
|
|
39
|
+
readonly accounts: readonly PluginAccountContext[];
|
|
40
|
+
readonly theme: PluginTheme;
|
|
41
|
+
};
|
|
42
|
+
sessionChanged: PluginSession;
|
|
43
|
+
sessionEnded: {
|
|
44
|
+
readonly reason: "signedOut" | "expired" | "unloaded";
|
|
45
|
+
};
|
|
46
|
+
accessChanged: {
|
|
47
|
+
readonly accounts: readonly PluginAccountContext[];
|
|
48
|
+
readonly entryAvailable: boolean;
|
|
49
|
+
};
|
|
50
|
+
themeChanged: PluginTheme;
|
|
51
|
+
}
|
|
52
|
+
export interface PluginToHostPayloads {
|
|
53
|
+
ready: {
|
|
54
|
+
readonly sdkVersion: string;
|
|
55
|
+
readonly protocolVersion: typeof PLUGIN_PROTOCOL_VERSION;
|
|
56
|
+
};
|
|
57
|
+
requestSessionRefresh: {
|
|
58
|
+
readonly reason: "unauthorized" | "expiring";
|
|
59
|
+
};
|
|
60
|
+
requestAccessRefresh: {
|
|
61
|
+
readonly reason: "forbidden" | "manual";
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export type PluginMessagePayloads = HostToPluginPayloads & PluginToHostPayloads;
|
|
65
|
+
export type PluginMessageType = keyof PluginMessagePayloads;
|
|
66
|
+
export type HostToPluginMessageType = keyof HostToPluginPayloads;
|
|
67
|
+
export type PluginToHostMessageType = keyof PluginToHostPayloads;
|
|
68
|
+
export declare const HOST_TO_PLUGIN_MESSAGE_TYPES: readonly HostToPluginMessageType[];
|
|
69
|
+
export declare const PLUGIN_TO_HOST_MESSAGE_TYPES: readonly PluginToHostMessageType[];
|
|
70
|
+
export interface PluginMessage<T extends PluginMessageType = PluginMessageType> {
|
|
71
|
+
readonly protocol: typeof PLUGIN_PROTOCOL;
|
|
72
|
+
readonly version: typeof PLUGIN_PROTOCOL_VERSION;
|
|
73
|
+
readonly type: T;
|
|
74
|
+
/** Per iframe instance; a reload gets a new one and replies for old ones are dropped. */
|
|
75
|
+
readonly instanceId: string;
|
|
76
|
+
readonly requestId?: string;
|
|
77
|
+
readonly payload: PluginMessagePayloads[T];
|
|
78
|
+
}
|
|
79
|
+
export type AnyPluginMessage = {
|
|
80
|
+
[T in PluginMessageType]: PluginMessage<T>;
|
|
81
|
+
}[PluginMessageType];
|
|
82
|
+
export declare function createPluginMessage<T extends PluginMessageType>(type: T, instanceId: string, payload: PluginMessagePayloads[T], requestId?: string): PluginMessage<T>;
|
|
83
|
+
export declare function isPluginSession(v: unknown): v is PluginSession;
|
|
84
|
+
export declare function isPluginAccountContext(v: unknown): v is PluginAccountContext;
|
|
85
|
+
/** Envelope + payload shape check. Returns null for anything that is not a well-formed message at the current version. */
|
|
86
|
+
export declare function parsePluginMessage(data: unknown): AnyPluginMessage | null;
|
|
87
|
+
export type HostToPluginMessage = {
|
|
88
|
+
[T in HostToPluginMessageType]: PluginMessage<T>;
|
|
89
|
+
}[HostToPluginMessageType];
|
|
90
|
+
export type PluginToHostMessage = {
|
|
91
|
+
[T in PluginToHostMessageType]: PluginMessage<T>;
|
|
92
|
+
}[PluginToHostMessageType];
|
|
93
|
+
export declare function isHostToPluginMessage(message: AnyPluginMessage): message is HostToPluginMessage;
|
|
94
|
+
export declare function isPluginToHostMessage(message: AnyPluginMessage): message is PluginToHostMessage;
|
|
95
|
+
export type PluginMessageInspection = {
|
|
96
|
+
readonly kind: "message";
|
|
97
|
+
readonly message: AnyPluginMessage;
|
|
98
|
+
} | {
|
|
99
|
+
readonly kind: "incompatible";
|
|
100
|
+
readonly version: unknown;
|
|
101
|
+
}
|
|
102
|
+
/** Not ours, or ours but malformed: either way there is nothing to act on. */
|
|
103
|
+
| {
|
|
104
|
+
readonly kind: "ignored";
|
|
105
|
+
};
|
|
106
|
+
/** Classifies `event.data` for both ends of the protocol: a well-formed message, our protocol at
|
|
107
|
+
* another version, or nothing to act on. Callers still check `event.source` and `event.origin`. */
|
|
108
|
+
export declare function inspectPluginMessage(data: unknown): PluginMessageInspection;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PLUGIN_TO_HOST_MESSAGE_TYPES = exports.HOST_TO_PLUGIN_MESSAGE_TYPES = exports.PLUGIN_HANDSHAKE_TIMEOUT_MS = exports.PLUGIN_PROTOCOL_VERSION = exports.PLUGIN_PROTOCOL = void 0;
|
|
4
|
+
exports.createPluginMessage = createPluginMessage;
|
|
5
|
+
exports.isPluginSession = isPluginSession;
|
|
6
|
+
exports.isPluginAccountContext = isPluginAccountContext;
|
|
7
|
+
exports.parsePluginMessage = parsePluginMessage;
|
|
8
|
+
exports.isHostToPluginMessage = isHostToPluginMessage;
|
|
9
|
+
exports.isPluginToHostMessage = isPluginToHostMessage;
|
|
10
|
+
exports.inspectPluginMessage = inspectPluginMessage;
|
|
11
|
+
const manifest_1 = require("./manifest");
|
|
12
|
+
const theme_1 = require("./theme");
|
|
13
|
+
exports.PLUGIN_PROTOCOL = "infra-client-plugin";
|
|
14
|
+
exports.PLUGIN_PROTOCOL_VERSION = 2;
|
|
15
|
+
exports.PLUGIN_HANDSHAKE_TIMEOUT_MS = 10_000;
|
|
16
|
+
exports.HOST_TO_PLUGIN_MESSAGE_TYPES = [
|
|
17
|
+
"init", "sessionChanged", "sessionEnded", "accessChanged", "themeChanged",
|
|
18
|
+
];
|
|
19
|
+
exports.PLUGIN_TO_HOST_MESSAGE_TYPES = [
|
|
20
|
+
"ready", "requestSessionRefresh", "requestAccessRefresh",
|
|
21
|
+
];
|
|
22
|
+
function createPluginMessage(type, instanceId, payload, requestId) {
|
|
23
|
+
return {
|
|
24
|
+
protocol: exports.PLUGIN_PROTOCOL,
|
|
25
|
+
version: exports.PLUGIN_PROTOCOL_VERSION,
|
|
26
|
+
type,
|
|
27
|
+
instanceId,
|
|
28
|
+
...(requestId === undefined ? {} : { requestId }),
|
|
29
|
+
payload,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
33
|
+
const isNonEmpty = (v) => typeof v === "string" && v.length > 0;
|
|
34
|
+
function isPluginSession(v) {
|
|
35
|
+
return isRecord(v) && isNonEmpty(v.token) && typeof v.expiresAt === "number" && Number.isFinite(v.expiresAt);
|
|
36
|
+
}
|
|
37
|
+
function isPluginAccountContext(v) {
|
|
38
|
+
return (isRecord(v) &&
|
|
39
|
+
typeof v.accountNumber === "string" && manifest_1.AWS_ACCOUNT_PATTERN.test(v.accountNumber) &&
|
|
40
|
+
typeof v.displayName === "string" &&
|
|
41
|
+
(v.regions === undefined || (Array.isArray(v.regions) && v.regions.every((r) => typeof r === "string"))) &&
|
|
42
|
+
Array.isArray(v.permissions) && v.permissions.every((p) => manifest_1.PLUGIN_PERMISSIONS.includes(p)));
|
|
43
|
+
}
|
|
44
|
+
function isAccountList(v) {
|
|
45
|
+
return Array.isArray(v) && v.every(isPluginAccountContext);
|
|
46
|
+
}
|
|
47
|
+
function isUserContext(v) {
|
|
48
|
+
return (isRecord(v) && isNonEmpty(v.subject) &&
|
|
49
|
+
(v.principalType === "user" || v.principalType === "service") &&
|
|
50
|
+
(v.email === undefined || typeof v.email === "string") &&
|
|
51
|
+
(v.displayName === undefined || typeof v.displayName === "string"));
|
|
52
|
+
}
|
|
53
|
+
function isPluginInfo(v) {
|
|
54
|
+
return isRecord(v) && isNonEmpty(v.id) && typeof v.displayName === "string" && typeof v.version === "string" && typeof v.revision === "string" && typeof v.apiBaseUrl === "string";
|
|
55
|
+
}
|
|
56
|
+
const PAYLOAD_CHECKS = {
|
|
57
|
+
init: (p) => isRecord(p) && isPluginSession(p.session) && isUserContext(p.user) && isPluginInfo(p.plugin) &&
|
|
58
|
+
(p.entry === "user" || p.entry === "admin") && isAccountList(p.accounts) &&
|
|
59
|
+
(0, theme_1.isPluginTheme)(p.theme),
|
|
60
|
+
sessionChanged: isPluginSession,
|
|
61
|
+
sessionEnded: (p) => isRecord(p) && (p.reason === "signedOut" || p.reason === "expired" || p.reason === "unloaded"),
|
|
62
|
+
accessChanged: (p) => isRecord(p) && isAccountList(p.accounts) && typeof p.entryAvailable === "boolean",
|
|
63
|
+
themeChanged: theme_1.isPluginTheme,
|
|
64
|
+
ready: (p) => isRecord(p) && isNonEmpty(p.sdkVersion) && p.protocolVersion === exports.PLUGIN_PROTOCOL_VERSION,
|
|
65
|
+
requestSessionRefresh: (p) => isRecord(p) && (p.reason === "unauthorized" || p.reason === "expiring"),
|
|
66
|
+
requestAccessRefresh: (p) => isRecord(p) && (p.reason === "forbidden" || p.reason === "manual"),
|
|
67
|
+
};
|
|
68
|
+
/** Envelope + payload shape check. Returns null for anything that is not a well-formed message at the current version. */
|
|
69
|
+
function parsePluginMessage(data) {
|
|
70
|
+
if (!isRecord(data))
|
|
71
|
+
return null;
|
|
72
|
+
if (data.protocol !== exports.PLUGIN_PROTOCOL || data.version !== exports.PLUGIN_PROTOCOL_VERSION)
|
|
73
|
+
return null;
|
|
74
|
+
// hasOwn, not `in`: `type: "__proto__"` would otherwise pass and be called as a validator.
|
|
75
|
+
if (typeof data.type !== "string" || !Object.hasOwn(PAYLOAD_CHECKS, data.type))
|
|
76
|
+
return null;
|
|
77
|
+
if (!isNonEmpty(data.instanceId))
|
|
78
|
+
return null;
|
|
79
|
+
if (data.requestId !== undefined && !isNonEmpty(data.requestId))
|
|
80
|
+
return null;
|
|
81
|
+
const type = data.type;
|
|
82
|
+
if (!PAYLOAD_CHECKS[type](data.payload))
|
|
83
|
+
return null;
|
|
84
|
+
return createPluginMessage(type, data.instanceId, data.payload, data.requestId);
|
|
85
|
+
}
|
|
86
|
+
const HOST_TO_PLUGIN_TYPE_SET = new Set(exports.HOST_TO_PLUGIN_MESSAGE_TYPES);
|
|
87
|
+
const PLUGIN_TO_HOST_TYPE_SET = new Set(exports.PLUGIN_TO_HOST_MESSAGE_TYPES);
|
|
88
|
+
function isHostToPluginMessage(message) {
|
|
89
|
+
return HOST_TO_PLUGIN_TYPE_SET.has(message.type);
|
|
90
|
+
}
|
|
91
|
+
function isPluginToHostMessage(message) {
|
|
92
|
+
return PLUGIN_TO_HOST_TYPE_SET.has(message.type);
|
|
93
|
+
}
|
|
94
|
+
/** Classifies `event.data` for both ends of the protocol: a well-formed message, our protocol at
|
|
95
|
+
* another version, or nothing to act on. Callers still check `event.source` and `event.origin`. */
|
|
96
|
+
function inspectPluginMessage(data) {
|
|
97
|
+
if (!isRecord(data) || data.protocol !== exports.PLUGIN_PROTOCOL)
|
|
98
|
+
return { kind: "ignored" };
|
|
99
|
+
if (data.version !== exports.PLUGIN_PROTOCOL_VERSION)
|
|
100
|
+
return { kind: "incompatible", version: data.version };
|
|
101
|
+
const message = parsePluginMessage(data);
|
|
102
|
+
return message ? { kind: "message", message } : { kind: "ignored" };
|
|
103
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const PLUGIN_API_ROOT = "/api/plugins";
|
|
2
|
+
export declare const PLUGIN_ASSET_ROOT = "/plugin-assets";
|
|
3
|
+
export declare function pluginApiBaseUrl(pluginId: string): string;
|
|
4
|
+
/** The gateway resource every route of a plugin hangs off: `/api/plugins/<id>/accounts/{accountNumber}`. */
|
|
5
|
+
export declare function pluginAccountRoot(pluginId: string): string;
|
|
6
|
+
/** The gateway resource path template for one registered route. */
|
|
7
|
+
export declare function pluginRouteResourcePath(pluginId: string, routePath: string): string;
|
|
8
|
+
/** The concrete account root a request lands on: the template with the account filled in. */
|
|
9
|
+
export declare function pluginAccountPath(pluginId: string, accountNumber: string): string;
|
|
10
|
+
export interface PluginResourcePath {
|
|
11
|
+
readonly pluginId: string;
|
|
12
|
+
/** The path the route was registered with, relative to the account root; "/" for the root. */
|
|
13
|
+
readonly routePath: string;
|
|
14
|
+
}
|
|
15
|
+
/** The inverse of `pluginRouteResourcePath`: the authorizer resolves a gateway resource template
|
|
16
|
+
* back to the plugin and route it belongs to. `null` for anything that is not one. */
|
|
17
|
+
export declare function parsePluginResourcePath(resource: string): PluginResourcePath | null;
|
|
18
|
+
/** Immutable asset prefix: a new hash for changed bytes, even under the same version. */
|
|
19
|
+
export declare function pluginAssetPrefix(pluginId: string, version: string, assetHash: string): string;
|
|
20
|
+
export declare function pluginUiUrl(assetPrefix: string, entry: string): string;
|