@agent-native/core 0.203.2-nightly-20261007213109 → 0.204.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/dist/a2a/handlers.js +57 -1
- package/dist/a2a/server.js +37 -1
- package/dist/action.d.ts +1 -0
- package/dist/action.js +26 -3
- package/dist/agent/contain-principal.d.ts +9 -0
- package/dist/agent/contain-principal.js +53 -0
- package/dist/agent/production-agent.js +32 -2
- package/dist/agent/run-manager.js +14 -2
- package/dist/agent/run-store.js +66 -12
- package/dist/audit/actions/export-audit-events.d.ts +1 -1
- package/dist/audit/actions/export-audit-events.js +2 -2
- package/dist/audit/actions/export-audit-ocsf.d.ts +11 -0
- package/dist/audit/actions/export-audit-ocsf.js +183 -0
- package/dist/audit/actions/list-audit-events.d.ts +1 -1
- package/dist/audit/actions/list-audit-events.js +3 -3
- package/dist/audit/config.d.ts +1 -1
- package/dist/audit/config.js +5 -1
- package/dist/audit/index.d.ts +1 -0
- package/dist/audit/index.js +1 -0
- package/dist/audit/ocsf.d.ts +70 -0
- package/dist/audit/ocsf.js +228 -0
- package/dist/audit/record.js +1 -1
- package/dist/audit/store.js +9 -2
- package/dist/audit/types.d.ts +12 -1
- package/dist/cli/agent.js +2 -11
- package/dist/cli/connect.js +7 -2
- package/dist/client/use-db-sync.d.ts +13 -0
- package/dist/client/use-db-sync.js +22 -1
- package/dist/coding-tools/run-code.js +22 -1
- package/dist/coding-tools/sandbox/local-child-process-adapter.js +20 -0
- package/dist/collab/client.js +36 -3
- package/dist/collab/routes.d.ts +9 -4
- package/dist/collab/routes.js +15 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.js +15 -8
- package/dist/deploy/workspace-deploy.js +88 -13
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/file-upload/builder.js +18 -4
- package/dist/framework-tools.js +3 -0
- package/dist/identity/rekey.js +6 -0
- package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
- package/dist/localization/core-messages/supplemental/ar-SA.js +15 -0
- package/dist/localization/core-messages/supplemental/de-DE.js +15 -0
- package/dist/localization/core-messages/supplemental/en-US.js +15 -0
- package/dist/localization/core-messages/supplemental/es-ES.js +15 -0
- package/dist/localization/core-messages/supplemental/fr-FR.js +15 -0
- package/dist/localization/core-messages/supplemental/hi-IN.js +15 -0
- package/dist/localization/core-messages/supplemental/ja-JP.js +15 -0
- package/dist/localization/core-messages/supplemental/ko-KR.js +15 -0
- package/dist/localization/core-messages/supplemental/pt-BR.js +15 -0
- package/dist/localization/core-messages/supplemental/zh-CN.js +15 -0
- package/dist/localization/core-messages/supplemental/zh-TW.js +15 -0
- package/dist/localization/mcp-settings-messages.d.ts +15 -0
- package/dist/mcp/actions/create-org-service-token.d.ts +8 -0
- package/dist/mcp/actions/create-org-service-token.js +93 -4
- package/dist/mcp/actions/list-org-service-tokens.d.ts +2 -0
- package/dist/mcp/actions/list-org-service-tokens.js +36 -8
- package/dist/mcp/actions/service-principal-input.d.ts +48 -0
- package/dist/mcp/actions/service-principal-input.js +102 -0
- package/dist/mcp/actions/set-service-principal-lifecycle.d.ts +15 -0
- package/dist/mcp/actions/set-service-principal-lifecycle.js +150 -0
- package/dist/mcp/actions/set-service-principal-policy.d.ts +14 -0
- package/dist/mcp/actions/set-service-principal-policy.js +105 -0
- package/dist/mcp/build-server.js +117 -13
- package/dist/mcp/connect-route.js +116 -4
- package/dist/mcp/connect-store.d.ts +7 -0
- package/dist/mcp/connect-store.js +21 -14
- package/dist/mcp-client/oauth-routes.js +2 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +7 -7
- package/dist/org/index.d.ts +2 -2
- package/dist/org/service-principal-guard.d.ts +64 -0
- package/dist/org/service-principal-guard.js +140 -0
- package/dist/org/service-principal-policy.d.ts +104 -0
- package/dist/org/service-principal-policy.js +272 -0
- package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
- package/dist/provider-api/actions/provider-api.d.ts +13 -13
- package/dist/scripts/dev/index.js +2 -8
- package/dist/scripts/parse-args.d.ts +7 -0
- package/dist/scripts/parse-args.js +76 -0
- package/dist/scripts/utils.d.ts +1 -1
- package/dist/scripts/utils.js +1 -1
- package/dist/secrets/routes.d.ts +11 -11
- package/dist/server/action-discovery.js +16 -42
- package/dist/server/agent-chat/script-entries.js +3 -8
- package/dist/server/agent-chat-plugin.js +8 -42
- package/dist/server/agent-engine-default-model-route.d.ts +2 -2
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/dist/server/auth.js +5 -2
- package/dist/server/core-routes-plugin.js +25 -6
- package/dist/server/google-oauth.js +8 -4
- package/dist/server/identity-sso.js +2 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/poll.d.ts +9 -0
- package/dist/server/poll.js +86 -7
- package/dist/server/query-echo-safe-redirect.d.ts +10 -0
- package/dist/server/query-echo-safe-redirect.js +102 -0
- package/dist/server/release-schema.js +4 -0
- package/dist/server/workspace-provider-oauth.js +2 -1
- package/dist/templates/chat/.agents/skills/actions/references/action-fields.md +1 -0
- package/dist/templates/default/.agents/skills/actions/references/action-fields.md +1 -0
- package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +24 -25
- package/dist/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +33 -0
- package/dist/templates/headless/.agents/skills/actions/references/action-fields.md +1 -0
- package/dist/templates/workspace-core/.agents/skills/actions/references/action-fields.md +1 -0
- package/dist/triggers/actions/manage-automation.d.ts +7 -7
- package/dist/vite/client.js +1 -0
- package/docs/content/audit-log.mdx +15 -0
- package/docs/content/external-agents-catalog.mdx +19 -0
- package/docs/content/http-api.mdx +21 -2
- package/docs/content/pr-visual-recap.mdx +3 -1
- package/docs/content/template-clips-embed.mdx +1 -1
- package/docs/content/turn-into-app.mdx +118 -0
- package/package.json +4 -4
- package/src/templates/chat/.agents/skills/actions/references/action-fields.md +1 -0
- package/src/templates/default/.agents/skills/actions/references/action-fields.md +1 -0
- package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +24 -25
- package/src/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +33 -0
- package/src/templates/headless/.agents/skills/actions/references/action-fields.md +1 -0
- package/src/templates/workspace-core/.agents/skills/actions/references/action-fields.md +1 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suspend, retire, or resume an org service principal.
|
|
3
|
+
*
|
|
4
|
+
* The committed lifecycle write is what blocks new work (admission, tool
|
|
5
|
+
* dispatch, and run start read it). Suspended and retired also contain the
|
|
6
|
+
* principal: in-flight agent runs it owns are aborted, and retired additionally
|
|
7
|
+
* revokes every active token. The write commits before containment runs, and a
|
|
8
|
+
* containment failure is returned (`containmentErrors`, `contained: false`)
|
|
9
|
+
* rather than swallowed: a partial containment must not read as complete.
|
|
10
|
+
*
|
|
11
|
+
* Owner/admin only, and not callable by the agent tool loop.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { defineAction } from "../../action.js";
|
|
15
|
+
import { containServicePrincipal, prepareServicePrincipalContainment, } from "../../agent/contain-principal.js";
|
|
16
|
+
import { orgAdminAudit, recordOrgAdminAuditEvent, } from "../../audit/org-admin.js";
|
|
17
|
+
import { SERVICE_PRINCIPAL_LIFECYCLES, ServicePrincipalRetiredError, getServicePrincipalPolicy, setServicePrincipalLifecycle, withServicePrincipalLifecycleLock, } from "../../org/service-principal-policy.js";
|
|
18
|
+
import { ensureConnectTables, revokeServiceTokensByName, } from "../connect-store.js";
|
|
19
|
+
import { describeServicePrincipal, parseServiceName, requireKnownServicePrincipal, } from "./service-principal-input.js";
|
|
20
|
+
import { requireServiceTokenCaller, ServiceTokenError, } from "./service-token-access.js";
|
|
21
|
+
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
22
|
+
export default defineAction({
|
|
23
|
+
description: "Suspend, retire, or resume an org service principal. Suspended and retired principals are refused at MCP admission and run start, and their in-flight agent runs are aborted; retired also revokes every active token. The result reports abortedRuns, revokedTokens, and any containmentErrors (contained: false means containment is incomplete — retry). Org owner/admin only.",
|
|
24
|
+
schema: z.object({
|
|
25
|
+
serviceName: z
|
|
26
|
+
.string()
|
|
27
|
+
.min(1)
|
|
28
|
+
.max(64)
|
|
29
|
+
.describe("Service name from list-org-service-tokens"),
|
|
30
|
+
lifecycle: z.enum(SERVICE_PRINCIPAL_LIFECYCLES),
|
|
31
|
+
reason: z.string().trim().max(500).optional(),
|
|
32
|
+
}),
|
|
33
|
+
toolCallable: false,
|
|
34
|
+
audit: orgAdminAudit({
|
|
35
|
+
targetType: "service-principal",
|
|
36
|
+
targetId: (args) => String(args.serviceName ?? ""),
|
|
37
|
+
summary: (args, result) => {
|
|
38
|
+
const r = result;
|
|
39
|
+
const errors = r.containmentErrors?.length ?? 0;
|
|
40
|
+
return `Set ${r.serviceEmail ?? args.serviceName} to ${args.lifecycle}: ${r.abortedRuns ?? 0} runs aborted, ${r.revokedTokens ?? 0} tokens revoked${errors ? `, ${errors} containment errors` : ""}`;
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
run: async (args, ctx) => {
|
|
44
|
+
const caller = await requireServiceTokenCaller({
|
|
45
|
+
userEmail: ctx?.userEmail,
|
|
46
|
+
orgId: ctx?.orgId,
|
|
47
|
+
level: "manage",
|
|
48
|
+
});
|
|
49
|
+
const serviceName = parseServiceName(args.serviceName);
|
|
50
|
+
await requireKnownServicePrincipal(caller.orgId, serviceName);
|
|
51
|
+
let policy;
|
|
52
|
+
let abortedRuns = 0;
|
|
53
|
+
let revokedTokens = 0;
|
|
54
|
+
let resumeBlocked = false;
|
|
55
|
+
let lifecycleCommitted = false;
|
|
56
|
+
const containmentErrors = [];
|
|
57
|
+
try {
|
|
58
|
+
if (args.lifecycle !== "active") {
|
|
59
|
+
await prepareServicePrincipalContainment();
|
|
60
|
+
if (args.lifecycle === "retired")
|
|
61
|
+
await ensureConnectTables();
|
|
62
|
+
policy = await withServicePrincipalLifecycleLock(caller.orgId, serviceName, (db) => setServicePrincipalLifecycle(caller.orgId, serviceName, args.lifecycle, { actorEmail: caller.email, reason: args.reason || null }, db));
|
|
63
|
+
lifecycleCommitted = true;
|
|
64
|
+
policy = await withServicePrincipalLifecycleLock(caller.orgId, serviceName, async (db) => {
|
|
65
|
+
const current = await getServicePrincipalPolicy(caller.orgId, serviceName, db);
|
|
66
|
+
if (!current || current.lifecycle !== args.lifecycle) {
|
|
67
|
+
throw new ServiceTokenError("The service principal lifecycle changed concurrently. Read its current state before retrying.", 409);
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const contained = await containServicePrincipal(caller.orgId, serviceName);
|
|
71
|
+
abortedRuns = contained.abortedRuns;
|
|
72
|
+
containmentErrors.push(...contained.containmentErrors);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
containmentErrors.push(`abort runs: ${errorMessage(error)}`);
|
|
76
|
+
}
|
|
77
|
+
if (args.lifecycle === "retired") {
|
|
78
|
+
try {
|
|
79
|
+
revokedTokens = await revokeServiceTokensByName(caller.orgId, serviceName);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
containmentErrors.push(`revoke tokens: ${errorMessage(error)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return current;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
policy = await withServicePrincipalLifecycleLock(caller.orgId, serviceName, async (db) => {
|
|
90
|
+
const current = await getServicePrincipalPolicy(caller.orgId, serviceName, db);
|
|
91
|
+
if (current?.lifecycle === "retired") {
|
|
92
|
+
throw new ServicePrincipalRetiredError();
|
|
93
|
+
}
|
|
94
|
+
if (current?.lifecycle === "suspended") {
|
|
95
|
+
try {
|
|
96
|
+
const contained = await containServicePrincipal(caller.orgId, serviceName);
|
|
97
|
+
abortedRuns = contained.abortedRuns;
|
|
98
|
+
containmentErrors.push(...contained.containmentErrors);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
containmentErrors.push(`abort runs: ${errorMessage(error)}`);
|
|
102
|
+
}
|
|
103
|
+
if (containmentErrors.length > 0) {
|
|
104
|
+
resumeBlocked = true;
|
|
105
|
+
return current;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return setServicePrincipalLifecycle(caller.orgId, serviceName, "active", { actorEmail: caller.email, reason: args.reason || null }, db);
|
|
109
|
+
});
|
|
110
|
+
if (resumeBlocked) {
|
|
111
|
+
throw new ServiceTokenError("The service principal could not be resumed because containment is incomplete. Retry after its in-flight runs stop.", 503);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error instanceof ServicePrincipalRetiredError) {
|
|
117
|
+
throw new ServiceTokenError(error.message, error.statusCode);
|
|
118
|
+
}
|
|
119
|
+
if (error instanceof ServiceTokenError)
|
|
120
|
+
throw error;
|
|
121
|
+
console.error("[service-principal] Lifecycle write failed:", error);
|
|
122
|
+
throw new ServiceTokenError(lifecycleCommitted
|
|
123
|
+
? "The lifecycle changed, but containment could not be confirmed. Retry the lifecycle request to finish containment."
|
|
124
|
+
: "Could not change the service principal lifecycle. Nothing was changed; try again.", 503);
|
|
125
|
+
}
|
|
126
|
+
const principal = describeServicePrincipal(caller.orgId, serviceName, policy);
|
|
127
|
+
if (args.lifecycle !== "active") {
|
|
128
|
+
await recordOrgAdminAuditEvent({
|
|
129
|
+
action: "contain-service-principal",
|
|
130
|
+
targetType: "service-principal",
|
|
131
|
+
targetId: serviceName,
|
|
132
|
+
summary: `Contained ${principal.serviceEmail}: ${abortedRuns} runs aborted, ${revokedTokens} tokens revoked${containmentErrors.length ? `, ${containmentErrors.length} errors` : ""}`,
|
|
133
|
+
userEmail: caller.email,
|
|
134
|
+
orgId: caller.orgId,
|
|
135
|
+
status: containmentErrors.length ? "error" : "success",
|
|
136
|
+
caller: ctx?.caller,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
orgId: caller.orgId,
|
|
141
|
+
serviceEmail: principal.serviceEmail,
|
|
142
|
+
lifecycle: args.lifecycle,
|
|
143
|
+
principal,
|
|
144
|
+
abortedRuns,
|
|
145
|
+
revokedTokens,
|
|
146
|
+
contained: containmentErrors.length === 0,
|
|
147
|
+
containmentErrors,
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
declare const _default: import("../../action.js").ActionDefinition<{
|
|
2
|
+
serviceName: string;
|
|
3
|
+
ownerEmail?: string;
|
|
4
|
+
team?: string;
|
|
5
|
+
riskTier?: "high" | "low" | "medium";
|
|
6
|
+
purpose?: string;
|
|
7
|
+
allowedActions?: string[];
|
|
8
|
+
}, {
|
|
9
|
+
orgId: string;
|
|
10
|
+
serviceEmail: string;
|
|
11
|
+
principal: import("./service-principal-input.js").ServicePrincipalView;
|
|
12
|
+
changedFields: ("allowedActions" | "ownerEmail" | "purpose" | "riskTier" | "team")[];
|
|
13
|
+
}>;
|
|
14
|
+
export default _default;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Set who is accountable for an org service principal and which actions it may
|
|
3
|
+
* call. Fields left out keep their stored value; `null` clears one (and for
|
|
4
|
+
* `allowedActions`, `null` means unrestricted). Lifecycle (suspend/resume) is a
|
|
5
|
+
* separate action because it also contains in-flight work.
|
|
6
|
+
*
|
|
7
|
+
* Owner/admin only, and not callable by the agent tool loop: a prompt-injected
|
|
8
|
+
* agent must not be able to widen a grant.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { defineAction } from "../../action.js";
|
|
12
|
+
import { orgAdminAudit } from "../../audit/org-admin.js";
|
|
13
|
+
import { upsertServicePrincipalPolicy, } from "../../org/service-principal-policy.js";
|
|
14
|
+
import { allowedActionsSchema, assertOwnerIsOrgMember, describeServicePrincipal, normalizeAllowedActions, ownerEmailSchema, parseServiceName, purposeSchema, requireKnownServicePrincipal, riskTierSchema, teamSchema, } from "./service-principal-input.js";
|
|
15
|
+
import { requireServiceTokenCaller, ServiceTokenError, } from "./service-token-access.js";
|
|
16
|
+
const FIELDS = [
|
|
17
|
+
"ownerEmail",
|
|
18
|
+
"team",
|
|
19
|
+
"riskTier",
|
|
20
|
+
"purpose",
|
|
21
|
+
"allowedActions",
|
|
22
|
+
];
|
|
23
|
+
export default defineAction({
|
|
24
|
+
description: "Set the governance record of an org service principal: accountable owner (an org member), team, risk tier, purpose, and the action allow-list (null = unrestricted, [] = nothing, a trailing * matches a prefix). Omitted fields are unchanged; null clears a field. Org owner/admin only.",
|
|
25
|
+
schema: z.object({
|
|
26
|
+
serviceName: z
|
|
27
|
+
.string()
|
|
28
|
+
.min(1)
|
|
29
|
+
.max(64)
|
|
30
|
+
.describe("Service name from list-org-service-tokens"),
|
|
31
|
+
ownerEmail: ownerEmailSchema.nullable().optional(),
|
|
32
|
+
team: teamSchema.nullable().optional(),
|
|
33
|
+
riskTier: riskTierSchema.optional(),
|
|
34
|
+
purpose: purposeSchema.nullable().optional(),
|
|
35
|
+
allowedActions: allowedActionsSchema.nullable().optional(),
|
|
36
|
+
}),
|
|
37
|
+
toolCallable: false,
|
|
38
|
+
audit: orgAdminAudit({
|
|
39
|
+
targetType: "service-principal",
|
|
40
|
+
targetId: (args) => String(args.serviceName ?? ""),
|
|
41
|
+
// The grant list is only ever counted, never echoed, into the event.
|
|
42
|
+
recordInputs: false,
|
|
43
|
+
summary: (args, result) => {
|
|
44
|
+
const r = result;
|
|
45
|
+
const changed = r.changedFields ?? [];
|
|
46
|
+
const detail = changed.map((f) => f === "allowedActions"
|
|
47
|
+
? args.allowedActions == null
|
|
48
|
+
? "allowedActions: unrestricted"
|
|
49
|
+
: `allowedActions: ${args.allowedActions.length} entries`
|
|
50
|
+
: f);
|
|
51
|
+
return `Updated governance of ${r.serviceEmail ?? args.serviceName}: ${detail.length ? detail.join(", ") : "no changes"}`;
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
run: async (args, ctx) => {
|
|
55
|
+
const caller = await requireServiceTokenCaller({
|
|
56
|
+
userEmail: ctx?.userEmail,
|
|
57
|
+
orgId: ctx?.orgId,
|
|
58
|
+
level: "manage",
|
|
59
|
+
});
|
|
60
|
+
const serviceName = parseServiceName(args.serviceName);
|
|
61
|
+
const existing = await requireKnownServicePrincipal(caller.orgId, serviceName);
|
|
62
|
+
const input = {};
|
|
63
|
+
if (args.ownerEmail !== undefined) {
|
|
64
|
+
input.ownerEmail =
|
|
65
|
+
args.ownerEmail === null
|
|
66
|
+
? null
|
|
67
|
+
: await assertOwnerIsOrgMember(caller.orgId, args.ownerEmail);
|
|
68
|
+
}
|
|
69
|
+
if (args.team !== undefined)
|
|
70
|
+
input.team = args.team;
|
|
71
|
+
if (args.riskTier !== undefined)
|
|
72
|
+
input.riskTier = args.riskTier;
|
|
73
|
+
if (args.purpose !== undefined)
|
|
74
|
+
input.purpose = args.purpose;
|
|
75
|
+
if (args.allowedActions !== undefined) {
|
|
76
|
+
input.allowedActions = normalizeAllowedActions(args.allowedActions);
|
|
77
|
+
}
|
|
78
|
+
const before = {
|
|
79
|
+
ownerEmail: existing?.ownerEmail ?? null,
|
|
80
|
+
team: existing?.team ?? null,
|
|
81
|
+
riskTier: existing?.riskTier ?? "medium",
|
|
82
|
+
purpose: existing?.purpose ?? null,
|
|
83
|
+
allowedActions: existing?.allowedActions ?? null,
|
|
84
|
+
};
|
|
85
|
+
const changedFields = FIELDS.filter((field) => input[field] !== undefined &&
|
|
86
|
+
JSON.stringify(input[field]) !== JSON.stringify(before[field]));
|
|
87
|
+
let saved = existing;
|
|
88
|
+
if (Object.keys(input).length > 0) {
|
|
89
|
+
try {
|
|
90
|
+
saved = await upsertServicePrincipalPolicy(caller.orgId, serviceName, input);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error("[service-principal] Policy write failed:", error);
|
|
94
|
+
throw new ServiceTokenError("Could not save the service principal policy. Nothing was changed; try again.", 503);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const principal = describeServicePrincipal(caller.orgId, serviceName, saved);
|
|
98
|
+
return {
|
|
99
|
+
orgId: caller.orgId,
|
|
100
|
+
serviceEmail: principal.serviceEmail,
|
|
101
|
+
principal,
|
|
102
|
+
changedFields,
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
});
|
package/dist/mcp/build-server.js
CHANGED
|
@@ -6,6 +6,8 @@ import { isActionContractError, isActionExposedToExternalAgents, isActionHiddenF
|
|
|
6
6
|
import { describeToolResultImages, extractAgentImagesFromActionResult, } from "../agent/tool-result-images.js";
|
|
7
7
|
import { getAppConfig } from "../app-config/store.js";
|
|
8
8
|
import { isMcpActionResult } from "../mcp-client/app-result.js";
|
|
9
|
+
import { assertServicePrincipalMayCall, assertServicePrincipalMayRun, recordServicePrincipalDenial, ServicePrincipalRefusedError, } from "../org/service-principal-guard.js";
|
|
10
|
+
import { isActionGranted } from "../org/service-principal-policy.js";
|
|
9
11
|
import { writeActionChangeMarker } from "../server/action-change-marker-write.js";
|
|
10
12
|
import { getConfiguredAppBasePath } from "../server/app-base-path.js";
|
|
11
13
|
import { readDeployCredentialEnv } from "../server/credential-provider.js";
|
|
@@ -1843,10 +1845,44 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
|
|
|
1843
1845
|
requestState,
|
|
1844
1846
|
});
|
|
1845
1847
|
}
|
|
1848
|
+
// Read per request, never cached: a principal suspended or re-scoped after
|
|
1849
|
+
// admission is stopped by the next list or call.
|
|
1850
|
+
async function resolveServiceGrant(actionName) {
|
|
1851
|
+
try {
|
|
1852
|
+
return await assertServicePrincipalMayRun(effectiveIdentity?.userEmail, typeof effectiveIdentity?.orgId === "string"
|
|
1853
|
+
? effectiveIdentity.orgId
|
|
1854
|
+
: undefined);
|
|
1855
|
+
}
|
|
1856
|
+
catch (error) {
|
|
1857
|
+
if (actionName && error instanceof ServicePrincipalRefusedError) {
|
|
1858
|
+
await recordServicePrincipalDenial({
|
|
1859
|
+
email: effectiveIdentity?.userEmail,
|
|
1860
|
+
orgId: effectiveIdentity?.orgId,
|
|
1861
|
+
actionName,
|
|
1862
|
+
caller: "mcp",
|
|
1863
|
+
error,
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
throw error;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
// MCP App widgets belong to their action: outside the grant, the resource is
|
|
1870
|
+
// as invisible as the tool.
|
|
1871
|
+
async function grantedAdvertisedActions(actionName) {
|
|
1872
|
+
const { allowedActions } = await resolveServiceGrant(actionName);
|
|
1873
|
+
return allowedActions === null
|
|
1874
|
+
? advertisedActions
|
|
1875
|
+
: Object.fromEntries(Object.entries(advertisedActions).filter(([name]) => isActionGranted(allowedActions, name)));
|
|
1876
|
+
}
|
|
1877
|
+
async function grantedResourceActions(actionName) {
|
|
1878
|
+
return grantedAdvertisedActions(actionName);
|
|
1879
|
+
}
|
|
1846
1880
|
server.setRequestHandler("tools/list", async (request, ctx) => {
|
|
1847
1881
|
const startedAt = Date.now();
|
|
1882
|
+
const { allowedActions } = await resolveServiceGrant("mcp:tools/list");
|
|
1848
1883
|
const result = await withCallerContext(async () => {
|
|
1849
1884
|
const tools = await Promise.all(Object.entries(advertisedActions)
|
|
1885
|
+
.filter(([name]) => isActionGranted(allowedActions, name))
|
|
1850
1886
|
.sort(([a], [b]) => compareMcpCatalogValues(a, b))
|
|
1851
1887
|
.map(async ([name, entry]) => {
|
|
1852
1888
|
const hasLink = typeof entry.link === "function";
|
|
@@ -1901,15 +1937,18 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
|
|
|
1901
1937
|
entry.tool.description ??
|
|
1902
1938
|
name;
|
|
1903
1939
|
const title = agentNativeToolTitle(name, entry.tool.title);
|
|
1904
|
-
const annotations =
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1940
|
+
const annotations = {
|
|
1941
|
+
title,
|
|
1942
|
+
...(entry.mcpAnnotations ??
|
|
1943
|
+
(directoryCatalog
|
|
1944
|
+
? undefined
|
|
1945
|
+
: {
|
|
1946
|
+
readOnlyHint: entry.readOnly === true,
|
|
1947
|
+
destructiveHint: entry.publicAgent?.isConsequential === true ||
|
|
1948
|
+
entry.needsApproval !== undefined,
|
|
1949
|
+
openWorldHint: false,
|
|
1950
|
+
})),
|
|
1951
|
+
};
|
|
1913
1952
|
if (directoryCatalog) {
|
|
1914
1953
|
delete annotations["agent-native/producesOpenLink"];
|
|
1915
1954
|
}
|
|
@@ -1942,6 +1981,7 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
|
|
|
1942
1981
|
tools.push(widgetSessionTool);
|
|
1943
1982
|
if (fullCatalogRequested &&
|
|
1944
1983
|
config.askAgent &&
|
|
1984
|
+
isActionGranted(allowedActions, "ask-agent") &&
|
|
1945
1985
|
hasMcpOAuthScope(effectiveIdentity?.oauthScopes, "mcp:write")) {
|
|
1946
1986
|
tools.push({
|
|
1947
1987
|
name: "ask-agent",
|
|
@@ -2004,6 +2044,31 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
|
|
|
2004
2044
|
: undefined;
|
|
2005
2045
|
const result = await withCallerContext(async () => {
|
|
2006
2046
|
const { name, arguments: args } = request.params;
|
|
2047
|
+
try {
|
|
2048
|
+
const { allowedActions } = await resolveServiceGrant();
|
|
2049
|
+
assertServicePrincipalMayCall(allowedActions, name);
|
|
2050
|
+
}
|
|
2051
|
+
catch (error) {
|
|
2052
|
+
if (!(error instanceof ServicePrincipalRefusedError))
|
|
2053
|
+
throw error;
|
|
2054
|
+
if (error.statusCode !== 403)
|
|
2055
|
+
throw error;
|
|
2056
|
+
failure = {
|
|
2057
|
+
errorType: error.errorCode,
|
|
2058
|
+
errorMessage: error.message,
|
|
2059
|
+
};
|
|
2060
|
+
await recordServicePrincipalDenial({
|
|
2061
|
+
email: effectiveIdentity?.userEmail,
|
|
2062
|
+
orgId: getRequestOrgId(),
|
|
2063
|
+
actionName: name,
|
|
2064
|
+
caller: "mcp",
|
|
2065
|
+
error,
|
|
2066
|
+
});
|
|
2067
|
+
return {
|
|
2068
|
+
content: [{ type: "text", text: error.message }],
|
|
2069
|
+
isError: true,
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2007
2072
|
if (name === "ask-agent" && config.askAgent) {
|
|
2008
2073
|
if (!fullCatalogRequested) {
|
|
2009
2074
|
failure = {
|
|
@@ -2299,8 +2364,9 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
|
|
|
2299
2364
|
if (supportsMcpApps) {
|
|
2300
2365
|
server.setRequestHandler("resources/list", async (request, ctx) => {
|
|
2301
2366
|
const startedAt = Date.now();
|
|
2367
|
+
const grantedActions = await grantedResourceActions("mcp:resources/list");
|
|
2302
2368
|
const result = await withCallerContext(async () => {
|
|
2303
|
-
const mcpAppResources = await getMcpAppResources(config,
|
|
2369
|
+
const mcpAppResources = await getMcpAppResources(config, grantedActions, effectiveIdentity, requestMeta);
|
|
2304
2370
|
return {
|
|
2305
2371
|
resources: mcpAppResources
|
|
2306
2372
|
.sort((a, b) => compareMcpCatalogValues(a.uri, b.uri))
|
|
@@ -2323,11 +2389,12 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
|
|
|
2323
2389
|
return result;
|
|
2324
2390
|
});
|
|
2325
2391
|
server.setRequestHandler("resources/templates/list", async () => {
|
|
2392
|
+
const grantedActions = await grantedResourceActions("mcp:resources/templates/list");
|
|
2326
2393
|
if (config.catalogMode === "directory") {
|
|
2327
2394
|
return withCallerContext(async () => ({ resourceTemplates: [] }));
|
|
2328
2395
|
}
|
|
2329
2396
|
return withCallerContext(async () => {
|
|
2330
|
-
const mcpAppResources = await getMcpAppResources(config,
|
|
2397
|
+
const mcpAppResources = await getMcpAppResources(config, grantedActions, effectiveIdentity, requestMeta);
|
|
2331
2398
|
return {
|
|
2332
2399
|
resourceTemplates: mcpAppResources
|
|
2333
2400
|
.sort((a, b) => compareMcpCatalogValues(a.uri, b.uri))
|
|
@@ -2359,11 +2426,12 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
|
|
|
2359
2426
|
});
|
|
2360
2427
|
};
|
|
2361
2428
|
try {
|
|
2429
|
+
const grantedActions = await grantedResourceActions("mcp:resources/read");
|
|
2362
2430
|
return await withCallerContext(async () => {
|
|
2363
2431
|
const uri = request.params?.uri;
|
|
2364
2432
|
let found = null;
|
|
2365
2433
|
const resourceActions = mcpAppWidgetsEnabledForIdentity(config, effectiveIdentity)
|
|
2366
|
-
? Object.entries(
|
|
2434
|
+
? Object.entries(grantedActions)
|
|
2367
2435
|
: [];
|
|
2368
2436
|
const orderedResourceActions = config.catalogMode === "directory"
|
|
2369
2437
|
? resourceActions.sort(([a], [b]) => compareMcpCatalogValues(a, b))
|
|
@@ -2671,6 +2739,42 @@ function matchesStoredConnectTokenIdentity(resolution, input) {
|
|
|
2671
2739
|
* organization.
|
|
2672
2740
|
*/
|
|
2673
2741
|
async function admitIssuedCredential(result, requestOrigin, orgResolution, issuedAt) {
|
|
2742
|
+
const admitted = await checkIssuedCredential(result, requestOrigin, orgResolution, issuedAt);
|
|
2743
|
+
return admitted.authed ? admitServicePrincipal(admitted) : admitted;
|
|
2744
|
+
}
|
|
2745
|
+
/**
|
|
2746
|
+
* A service identity is admitted only while its governance record says it may
|
|
2747
|
+
* run. Every credential branch that can yield one ends here; a caller whose
|
|
2748
|
+
* email is not service-shaped returns before any database read.
|
|
2749
|
+
*/
|
|
2750
|
+
async function admitServicePrincipal(result) {
|
|
2751
|
+
const email = result.identity?.userEmail;
|
|
2752
|
+
try {
|
|
2753
|
+
await assertServicePrincipalMayRun(email, typeof result.identity?.orgId === "string"
|
|
2754
|
+
? result.identity.orgId
|
|
2755
|
+
: undefined);
|
|
2756
|
+
return result;
|
|
2757
|
+
}
|
|
2758
|
+
catch (error) {
|
|
2759
|
+
if (!(error instanceof ServicePrincipalRefusedError))
|
|
2760
|
+
throw error;
|
|
2761
|
+
if (error.statusCode === 403) {
|
|
2762
|
+
await recordServicePrincipalDenial({
|
|
2763
|
+
email,
|
|
2764
|
+
orgId: typeof result.identity?.orgId === "string"
|
|
2765
|
+
? result.identity.orgId
|
|
2766
|
+
: undefined,
|
|
2767
|
+
actionName: "mcp:admission",
|
|
2768
|
+
caller: "mcp",
|
|
2769
|
+
error,
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
return error.statusCode === 503
|
|
2773
|
+
? { authed: false, unavailable: true }
|
|
2774
|
+
: { authed: false };
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
async function checkIssuedCredential(result, requestOrigin, orgResolution, issuedAt) {
|
|
2674
2778
|
const { checkCredentialEmailRetirement, checkCredentialOrgMembership } = await import("./credential-membership.js");
|
|
2675
2779
|
if (result.identity.userEmail) {
|
|
2676
2780
|
const retirement = await checkCredentialEmailRetirement({
|
|
@@ -2862,7 +2966,7 @@ export async function verifyAuth(authHeader, ownerEmailHeader, options = {}) {
|
|
|
2862
2966
|
// sibling app per call, so they are cross-app A2A tokens.
|
|
2863
2967
|
const admitted = tokenScope === MCP_CONNECT_SCOPE && !firstPartyMcp
|
|
2864
2968
|
? await admitIssuedCredential(verified, options.requestOrigin, orgResolution, typeof payload.iat === "number" ? payload.iat : undefined)
|
|
2865
|
-
: verified;
|
|
2969
|
+
: await admitServicePrincipal(verified);
|
|
2866
2970
|
if (admitted.authed && tokenScope === MCP_CONNECT_SCOPE) {
|
|
2867
2971
|
await markConnectTokenUsed(payload.jti);
|
|
2868
2972
|
}
|
|
@@ -9,9 +9,11 @@ import { getOrgDomain } from "../org/context.js";
|
|
|
9
9
|
import { getSession, getConfiguredLoginHtml, isLoopbackRequest, } from "../server/auth.js";
|
|
10
10
|
import { CREDENTIAL_MEMBERSHIP_UNAVAILABLE_MESSAGE } from "../server/credential-membership-unavailable.js";
|
|
11
11
|
import { readDeployCredentialEnv } from "../server/credential-provider.js";
|
|
12
|
+
import { publicFrameworkPath } from "../server/framework-route-prefix.js";
|
|
12
13
|
import { readBody } from "../server/h3-helpers.js";
|
|
14
|
+
import { FRAMEWORK_INTERNAL_ROUTE_PREFIX } from "../shared/framework-route-prefix.js";
|
|
13
15
|
import { MCP_CONNECT_MCP_URL_TEMPLATE, getMcpConnectGuides, getMcpStaticTokenFallback, interpolateMcpConnectTemplate, resolveMcpConnectGuideId, } from "../shared/mcp-connect-content.js";
|
|
14
|
-
import { ensureConnectTables, recordMintedToken, listTokens, revokeToken, normalizeServiceName, serviceIdentityEmail, createDeviceCode, getDeviceCode, getDeviceCodeByUserCode, approveDeviceCode, consumeDeviceCode, claimDeviceCodeForMint, finishDeviceCodeMint, expireDeviceCode, MCP_CONNECT_OAUTH_CLIENT_ID, MCP_CONNECT_SCOPE, DEFAULT_TOKEN_TTL_DAYS, MIN_TOKEN_TTL_DAYS, MAX_TOKEN_TTL_DAYS, DEVICE_CODE_TTL_MS, } from "./connect-store.js";
|
|
16
|
+
import { ensureConnectTables, recordMintedToken, listTokens, revokeToken, normalizeServiceName, serviceIdentityEmail, createDeviceCode, getDeviceCode, getDeviceCodeByUserCode, approveDeviceCode, consumeDeviceCode, claimDeviceCodeForMint, finishDeviceCodeMint, expireDeviceCode, MCP_CONNECT_OAUTH_CLIENT_ID, MCP_CONNECT_SCOPE, DEFAULT_TOKEN_TTL_DAYS, MIN_TOKEN_TTL_DAYS, MAX_TOKEN_TTL_DAYS, MAX_SERVICE_TOKEN_TTL_DAYS, DEVICE_CODE_TTL_MS, } from "./connect-store.js";
|
|
15
17
|
import { McpCredentialIssuanceError, withMcpCredentialIssuance, } from "./credential-issuance.js";
|
|
16
18
|
import { MCP_OAUTH_DEFAULT_SCOPE, signMcpOAuthAccessToken, } from "./oauth-token.js";
|
|
17
19
|
import { resolveMcpOrgChoices } from "./org-choice.js";
|
|
@@ -131,11 +133,11 @@ async function resolveOrgDomain(orgId) {
|
|
|
131
133
|
return undefined;
|
|
132
134
|
}
|
|
133
135
|
}
|
|
134
|
-
function clampTtlDays(input) {
|
|
136
|
+
function clampTtlDays(input, maxDays = MAX_TOKEN_TTL_DAYS) {
|
|
135
137
|
const n = Number(input);
|
|
136
138
|
if (!Number.isFinite(n))
|
|
137
139
|
return DEFAULT_TOKEN_TTL_DAYS;
|
|
138
|
-
return Math.min(
|
|
140
|
+
return Math.min(maxDays, Math.max(MIN_TOKEN_TTL_DAYS, Math.floor(n)));
|
|
139
141
|
}
|
|
140
142
|
/**
|
|
141
143
|
* Mint a connect-scoped JWT and record it. The token value is returned to the
|
|
@@ -218,7 +220,7 @@ export async function mintOrgServiceToken(params) {
|
|
|
218
220
|
const serviceName = normalizeServiceName(params.serviceName);
|
|
219
221
|
const serviceEmail = serviceIdentityEmail(serviceName, params.orgId);
|
|
220
222
|
const orgDomain = await resolveOrgDomain(params.orgId);
|
|
221
|
-
const ttlDays = clampTtlDays(params.ttlDays ?? DEFAULT_TOKEN_TTL_DAYS);
|
|
223
|
+
const ttlDays = clampTtlDays(params.ttlDays ?? DEFAULT_TOKEN_TTL_DAYS, MAX_SERVICE_TOKEN_TTL_DAYS);
|
|
222
224
|
await prepareConnectIssuance();
|
|
223
225
|
return withMcpCredentialIssuance({
|
|
224
226
|
email: params.createdBy,
|
|
@@ -605,6 +607,10 @@ function renderConnectPage(params) {
|
|
|
605
607
|
.tok:last-child { border-bottom: none; }
|
|
606
608
|
.tok .meta { color: var(--subtle); font-size: 0.74rem; margin-top: 0.1rem; }
|
|
607
609
|
.tok.revoked { opacity: 0.45; }
|
|
610
|
+
.pstate { font-size: 0.68rem; padding: 0.05rem 0.4rem; margin-inline-start: 0.4rem;
|
|
611
|
+
border: 1px solid var(--border); border-radius: 999px; color: var(--subtle); }
|
|
612
|
+
.pstate.active { color: var(--ok); }
|
|
613
|
+
.pstate.suspended { color: var(--error); }
|
|
608
614
|
.empty-state {
|
|
609
615
|
color: var(--subtle); font-size: 0.78rem; line-height: 1.45;
|
|
610
616
|
padding: 0.3rem 0 0.45rem;
|
|
@@ -821,11 +827,22 @@ function renderConnectPage(params) {
|
|
|
821
827
|
</summary>
|
|
822
828
|
<div id="tokenList" class="token-list"><div class="empty-state">${localize(connectMessages.checkingConnections)}</div></div>
|
|
823
829
|
</details>
|
|
830
|
+
|
|
831
|
+
<details id="principals" class="connections hidden">
|
|
832
|
+
<summary>
|
|
833
|
+
<span class="connections-title">${localize(connectMessages.servicePrincipals)}</span>
|
|
834
|
+
<span id="principalsState" class="connections-state hidden" aria-live="polite"></span>
|
|
835
|
+
<span class="chev" aria-hidden="true"></span>
|
|
836
|
+
</summary>
|
|
837
|
+
<div id="principalMsg" class="empty-state hidden" role="status"></div>
|
|
838
|
+
<div id="principalList" class="token-list"></div>
|
|
839
|
+
</details>
|
|
824
840
|
</div>
|
|
825
841
|
<script>
|
|
826
842
|
(function () {
|
|
827
843
|
var BASE = ${JSON.stringify(joinAppPath(connectBasePath, MCP_PUBLIC_ROUTE_PREFIX + "/connect"))};
|
|
828
844
|
var USER_CODE = ${JSON.stringify(safeUserCode || null)};
|
|
845
|
+
var ACTIONS = ${JSON.stringify(joinAppPath(connectBasePath, publicFrameworkPath(`${FRAMEWORK_INTERNAL_ROUTE_PREFIX}/actions`)))};
|
|
829
846
|
var COPY = ${JSON.stringify(connectMessages)};
|
|
830
847
|
var msgEl = document.getElementById("msg");
|
|
831
848
|
var connectionsEl = document.getElementById("connections");
|
|
@@ -988,6 +1005,100 @@ function renderConnectPage(params) {
|
|
|
988
1005
|
}
|
|
989
1006
|
}
|
|
990
1007
|
|
|
1008
|
+
// Governance view over the org's service principals. Hidden when the caller
|
|
1009
|
+
// has no org (400/401/403); route failures are shown, never read as "none".
|
|
1010
|
+
var principalsEl = document.getElementById("principals");
|
|
1011
|
+
var principalsStateEl = document.getElementById("principalsState");
|
|
1012
|
+
var principalListEl = document.getElementById("principalList");
|
|
1013
|
+
var principalMsgEl = document.getElementById("principalMsg");
|
|
1014
|
+
var PRINCIPAL_STATES = {
|
|
1015
|
+
ungoverned: "principalUngoverned", active: "principalActive",
|
|
1016
|
+
suspended: "principalSuspended", retired: "principalRetired"
|
|
1017
|
+
};
|
|
1018
|
+
var RISK_TIERS = { low: "riskLow", medium: "riskMedium", high: "riskHigh" };
|
|
1019
|
+
|
|
1020
|
+
function principalNote(text) {
|
|
1021
|
+
principalMsgEl.textContent = text;
|
|
1022
|
+
principalMsgEl.classList.toggle("hidden", !text);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
async function setLifecycle(name, lifecycle, btn) {
|
|
1026
|
+
btn.disabled = true;
|
|
1027
|
+
var r = await postActionJson("set-service-principal-lifecycle", { serviceName: name, lifecycle: lifecycle });
|
|
1028
|
+
await loadPrincipals();
|
|
1029
|
+
if (!r.ok) principalNote(COPY.couldNotUpdatePrincipal);
|
|
1030
|
+
else if (r.data && r.data.contained === false) principalNote(COPY.containmentIncomplete);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
async function postActionJson(name, body) {
|
|
1034
|
+
var res = await fetch(ACTIONS + "/" + name, {
|
|
1035
|
+
method: "POST",
|
|
1036
|
+
headers: { "Content-Type": "application/json" },
|
|
1037
|
+
credentials: "same-origin",
|
|
1038
|
+
body: JSON.stringify(body)
|
|
1039
|
+
});
|
|
1040
|
+
var data = null;
|
|
1041
|
+
var unreadable = false;
|
|
1042
|
+
try { data = await res.json(); } catch (e) { unreadable = true; }
|
|
1043
|
+
return { ok: res.ok && !unreadable, status: res.status, data: data };
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
async function loadPrincipals() {
|
|
1047
|
+
principalNote("");
|
|
1048
|
+
try {
|
|
1049
|
+
var res = await fetch(ACTIONS + "/list-org-service-tokens", { credentials: "same-origin" });
|
|
1050
|
+
if (res.status === 400 || res.status === 401 || res.status === 403) {
|
|
1051
|
+
principalsEl.classList.add("hidden");
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
1055
|
+
var data = await res.json();
|
|
1056
|
+
var principals = (data && data.principals) || [];
|
|
1057
|
+
if (!principals.length) {
|
|
1058
|
+
principalsEl.classList.add("hidden");
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
principalsEl.classList.remove("hidden");
|
|
1062
|
+
var needAction = principals.filter(function (p) { return p.state === "ungoverned"; }).length;
|
|
1063
|
+
principalsStateEl.textContent = needAction ? String(needAction) : "";
|
|
1064
|
+
principalsStateEl.classList.toggle("hidden", needAction === 0);
|
|
1065
|
+
principalListEl.innerHTML = "";
|
|
1066
|
+
principals.forEach(function (p) {
|
|
1067
|
+
var div = document.createElement("div");
|
|
1068
|
+
div.className = "tok" + (p.state === "retired" ? " revoked" : "");
|
|
1069
|
+
var left = document.createElement("div");
|
|
1070
|
+
var title = document.createElement("div");
|
|
1071
|
+
title.textContent = p.serviceName;
|
|
1072
|
+
var badge = document.createElement("span");
|
|
1073
|
+
badge.className = "pstate " + p.state;
|
|
1074
|
+
badge.textContent = COPY[PRINCIPAL_STATES[p.state]] || p.state;
|
|
1075
|
+
title.appendChild(badge);
|
|
1076
|
+
var meta = document.createElement("div");
|
|
1077
|
+
meta.className = "meta";
|
|
1078
|
+
meta.textContent = p.state === "ungoverned"
|
|
1079
|
+
? COPY.principalUngovernedHint
|
|
1080
|
+
: (p.ownerEmail ? COPY.principalOwner + ": " + p.ownerEmail + " · " : "") +
|
|
1081
|
+
COPY.principalRisk + ": " + (COPY[RISK_TIERS[p.riskTier]] || p.riskTier);
|
|
1082
|
+
left.appendChild(title); left.appendChild(meta);
|
|
1083
|
+
div.appendChild(left);
|
|
1084
|
+
var next = p.state === "suspended" ? "active" : p.state === "retired" ? null : "suspended";
|
|
1085
|
+
if (data.canManage && next) {
|
|
1086
|
+
var btn = document.createElement("button");
|
|
1087
|
+
btn.className = "ghost";
|
|
1088
|
+
btn.textContent = next === "active" ? COPY.resume : COPY.suspend;
|
|
1089
|
+
btn.onclick = function () { setLifecycle(p.serviceName, next, btn).catch(function () { btn.disabled = false; principalNote(COPY.couldNotUpdatePrincipal); }); };
|
|
1090
|
+
div.appendChild(btn);
|
|
1091
|
+
}
|
|
1092
|
+
principalListEl.appendChild(div);
|
|
1093
|
+
});
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
principalsEl.classList.remove("hidden");
|
|
1096
|
+
principalsStateEl.textContent = COPY.unavailable;
|
|
1097
|
+
principalsStateEl.classList.remove("hidden");
|
|
1098
|
+
principalNote(COPY.couldNotLoadConnections);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
991
1102
|
document.getElementById("authorizeBtn").onclick = async function () {
|
|
992
1103
|
var btn = this;
|
|
993
1104
|
setButtonLoading(btn, USER_CODE ? COPY.authorizingDevice : COPY.creatingToken);
|
|
@@ -1078,6 +1189,7 @@ function renderConnectPage(params) {
|
|
|
1078
1189
|
};
|
|
1079
1190
|
|
|
1080
1191
|
loadTokens();
|
|
1192
|
+
if (!USER_CODE) loadPrincipals();
|
|
1081
1193
|
})();
|
|
1082
1194
|
</script>
|
|
1083
1195
|
</body>
|