@k2b/cloud 0.43.0 → 0.44.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/package.json +7 -2
- package/src/_internal/app-access-request.ts +13 -0
- package/src/_internal/app-access-resources.ts +141 -0
- package/src/_internal/app-event-catalog.ts +40 -0
- package/src/_internal/app-event-schemas.ts +127 -0
- package/src/_internal/application-package.ts +40 -0
- package/src/_internal/built-in-app-ids.json +27 -0
- package/src/_internal/capabilities.ts +158 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +7 -5
- package/src/_internal/define-app.ts +57 -6
- package/src/_internal/object-store-area.ts +9 -0
- package/src/_internal/object-store-config.ts +101 -0
- package/src/_internal/object-store-key.ts +14 -0
- package/src/_internal/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +11 -1
- package/src/_internal/registry.ts +8 -1
- package/src/_internal/runtime-context.ts +8 -1
- package/src/access/AppAccessEditor.tsx +2 -0
- package/src/access/PermissionEditor.tsx +9 -3
- package/src/access/PrincipalPicker.tsx +14 -4
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-messages.ts +6 -0
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +7 -0
- package/src/ai/admin.ts +3 -0
- package/src/ai/app-frame.ts +12 -0
- package/src/ai/application-tasks.ts +101 -0
- package/src/ai/assistant-models.ts +126 -64
- package/src/ai/capabilities.ts +19 -4
- package/src/ai/chart-block.ts +63 -30
- package/src/ai/chat/chart-block.tsx +12 -3
- package/src/ai/chat/composer-adapter.ts +8 -7
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +2 -2
- package/src/ai/chat/presentation.tsx +22 -1
- package/src/ai/chat-quotas.ts +5 -4
- package/src/ai/chat-tasks.ts +2 -2
- package/src/ai/client/controller.ts +11 -10
- package/src/ai/client/projection.ts +3 -3
- package/src/ai/compaction.ts +3 -3
- package/src/ai/credentials.ts +13 -114
- package/src/ai/default-tool-contracts.ts +4 -3
- package/src/ai/dictation-runtime.ts +4 -4
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +31 -22
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +11 -5
- package/src/ai/inference-calls.ts +5 -4
- package/src/ai/maintenance.ts +1 -1
- package/src/ai/memory-learning-runs.ts +5 -5
- package/src/ai/memory-learning.ts +3 -3
- package/src/ai/message-queue.ts +1 -2
- package/src/ai/migrate.ts +75 -42
- package/src/ai/model-access-migrate.ts +3 -17
- package/src/ai/model-access.ts +44 -85
- package/src/ai/model-card.tsx +134 -0
- package/src/ai/model-catalog.ts +359 -0
- package/src/ai/model-chooser-messages.ts +88 -0
- package/src/ai/model-chooser.tsx +577 -0
- package/src/ai/model-delete.ts +234 -0
- package/src/ai/model-fixture.ts +39 -0
- package/src/ai/model-icon.tsx +36 -0
- package/src/ai/model-overview.ts +53 -0
- package/src/ai/model-usage.ts +45 -0
- package/src/ai/openai-api-messages.ts +0 -4
- package/src/ai/openai-api.ts +26 -9
- package/src/ai/personal-agent.ts +1 -1
- package/src/ai/prefs.ts +12 -10
- package/src/ai/projects-routes.ts +2 -1
- package/src/ai/projects.ts +13 -12
- package/src/ai/protocol.ts +2 -2
- package/src/ai/provider-connections.ts +161 -0
- package/src/ai/provider-errors.ts +55 -6
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider.ts +1 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +4 -2
- package/src/ai/quota-report.ts +7 -4
- package/src/ai/quotas.ts +19 -8
- package/src/ai/recent-resources.ts +6 -0
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +17 -18
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/store.ts +45 -23
- package/src/ai/stream.ts +1 -1
- package/src/ai/structured-runs.ts +2 -2
- package/src/ai/structured.ts +21 -44
- package/src/ai/task-contracts.ts +25 -5
- package/src/ai/task-execution.ts +15 -6
- package/src/ai/task-model.ts +9 -0
- package/src/ai/transcription.ts +27 -22
- package/src/ai/types.ts +50 -62
- package/src/ai/ui.tsx +13 -0
- package/src/ai/usage.ts +8 -8
- package/src/ai/validate.ts +8 -7
- package/src/ai/view-block.ts +2 -2
- package/src/api/accounts-entities.ts +16 -0
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-usage.ts +1 -1
- package/src/api/admin-ai.ts +173 -0
- package/src/api/admin-core-settings.ts +68 -244
- package/src/api/app-approval.ts +3 -1
- package/src/api/app-event-delivery.ts +51 -0
- package/src/api/app-events.ts +123 -0
- package/src/api/app-resource-access.ts +48 -0
- package/src/api/auth/account-mismatch.ts +24 -0
- package/src/api/auth.ts +26 -3
- package/src/api/capabilities.ts +28 -4
- package/src/api/index.ts +7 -1
- package/src/api/mcp.ts +175 -2
- package/src/api/me.ts +6 -4
- package/src/api/pwa-phone.ts +75 -26
- package/src/api/pwa.ts +34 -8
- package/src/api/resource-links.ts +83 -6
- package/src/api/resource-resolve.ts +50 -0
- package/src/api/search/schemas.ts +3 -1
- package/src/api/search.ts +9 -7
- package/src/app-pwa/contracts.ts +4 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +1 -0
- package/src/app-pwa/service.ts +91 -0
- package/src/app-pwa/transport.ts +22 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/capabilities/server-invoke.ts +2 -11
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- package/src/cli/admin/ai-models.ts +199 -0
- package/src/cli/admin/ai-quotas.ts +6 -102
- package/src/cli/admin/ai-usage.ts +4 -4
- package/src/cli/admin/app-credentials.ts +5 -3
- package/src/cli/admin/index.ts +5 -2
- package/src/cli/apps.ts +89 -26
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +26 -2
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +21 -5
- package/src/contracts/capabilities.ts +143 -3
- package/src/contracts/index.ts +2 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/pwa.ts +17 -1
- package/src/contracts/registry.ts +6 -0
- package/src/contracts/resource-clipboard.ts +35 -0
- package/src/contracts/resource-pages.ts +140 -0
- package/src/contracts/resource-resolution.ts +13 -0
- package/src/contracts/shared.ts +4 -0
- package/src/contracts/user-notifications.ts +2 -0
- package/src/events/app-event-delivery.ts +100 -0
- package/src/events/app-events.testing.ts +65 -0
- package/src/events/app-events.ts +134 -0
- package/src/events/core-user-created.ts +24 -0
- package/src/events/index.ts +9 -0
- package/src/events/testing.ts +2 -0
- package/src/index.ts +1 -1
- package/src/server/api/respond.ts +8 -3
- package/src/server/index.ts +1 -0
- package/src/server/middleware/app-access.ts +1 -1
- package/src/server/middleware/auth.ts +21 -7
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +2 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +8 -0
- package/src/services/accounts/app.ts +6 -0
- package/src/services/accounts/entities.ts +14 -3
- package/src/services/accounts/groups.ts +3 -1
- package/src/services/accounts/identities.ts +62 -10
- package/src/services/accounts/users.ts +7 -6
- package/src/services/app-access.ts +25 -7
- package/src/services/app-details.ts +224 -0
- package/src/services/app-discovery.ts +2 -1
- package/src/services/app-event-shapes.ts +139 -0
- package/src/services/app-events/accept.ts +116 -0
- package/src/services/app-events/admin.ts +250 -0
- package/src/services/app-events/declarations.ts +187 -0
- package/src/services/app-events/deliver.ts +218 -0
- package/src/services/app-events.ts +92 -0
- package/src/services/config-conflict.ts +11 -0
- package/src/services/core-user-events.ts +1 -0
- package/src/services/freeipa-config.ts +9 -0
- package/src/services/geo/ledger.ts +28 -10
- package/src/services/geo/service.ts +43 -24
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/invocation-authority.ts +4 -0
- package/src/services/identity/invocation-token.ts +238 -56
- package/src/services/identity/issue-invocation.ts +6 -1
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +13 -1
- package/src/services/ipa/sync.ts +6 -2
- package/src/services/ipa/users.ts +4 -1
- package/src/services/mandates/policy.ts +19 -12
- package/src/services/notifications/browser.ts +4 -0
- package/src/services/notifications/catalog.ts +5 -2
- package/src/services/notifications/platform.ts +7 -2
- package/src/services/notifications/user.ts +18 -7
- package/src/services/oauth-tokens.ts +28 -2
- package/src/services/object-store.ts +217 -0
- package/src/services/providers/local/users.ts +5 -0
- package/src/services/pwa-devices.ts +147 -37
- package/src/services/session/index.ts +14 -0
- package/src/services/settings/core-settings.ts +71 -43
- package/src/services/settings/index.ts +8 -0
- package/src/services/settings/versions.ts +26 -0
- package/src/shared/ai-models.ts +274 -0
- package/src/shared/ai-quotas.ts +2 -2
- package/src/shared/ai-usage.ts +2 -2
- package/src/shared/index.ts +2 -0
- package/src/shared-text/documents.ts +25 -8
- package/src/shared-text/log.ts +1 -1
- package/src/shared-text/text-diff.ts +75 -0
- package/src/ssr/AdminArea.tsx +131 -0
- package/src/ssr/AdminLayout.tsx +9 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +2 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/CloudShell.island.tsx +25 -4
- package/src/ssr/Layout.tsx +8 -6
- package/src/ssr/PwaLayout.tsx +16 -1
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +25 -20
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/index.ts +8 -0
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +9 -1
- package/src/ssr/pwa-messages.ts +2 -2
- package/src/ssr/rail-navigation.ts +5 -1
- package/src/ssr/settings-draft.ts +153 -0
- package/src/ssr/shell-data.tsx +2 -0
- package/src/styles/global.css +1 -0
- package/src/styles/profile-picture.css +270 -0
- package/src/styles/utilities-navigation.css +6 -0
- package/src/workflows/ai/policy.ts +49 -0
- package/src/workflows/ai/runtime.ts +131 -21
- package/src/workflows/ai/store.ts +283 -35
- package/src/workflows/ai/types.ts +8 -2
- package/src/workflows/ai-actions.ts +148 -120
- package/src/workflows/authoring.ts +31 -0
- package/src/workflows/contracts.ts +1 -0
- package/src/workflows/definition.ts +2 -0
- package/src/workflows/editor.ts +15 -0
- package/src/workflows/index.ts +2 -1
- package/src/workflows/language/compiler.ts +34 -3
- package/src/workflows/runtime/executor.ts +132 -33
- package/src/workflows/runtime/index.ts +2 -0
- package/src/workflows/runtime/ports.ts +5 -1
- package/src/workflows/store/actions.ts +1 -1
- package/src/workflows/store/definitions.ts +5 -0
- package/src/workflows/store/runs.ts +5 -3
- package/src/workflows/store/worker.ts +7 -2
- package/src/ai/model-pricing.ts +0 -43
- package/src/ai/model-request-settings.ts +0 -98
- package/src/ai/settings-messages.ts +0 -129
- package/src/shared/ai-model-request-settings.ts +0 -21
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { isDeepStrictEqual } from "node:util";
|
|
2
3
|
import { createRemoteJWKSet, decodeProtectedHeader, errors, type JWTVerifyGetKey, jwtVerify, SignJWT } from "jose";
|
|
3
4
|
import { z } from "zod";
|
|
@@ -46,7 +47,7 @@ export const normalizeInvocationRequestId = (value: unknown): string | undefined
|
|
|
46
47
|
return parsed.success ? parsed.data : undefined;
|
|
47
48
|
};
|
|
48
49
|
|
|
49
|
-
const
|
|
50
|
+
const InvocationPayloadBaseSchema = z
|
|
50
51
|
.object({
|
|
51
52
|
iss: z.string().url(),
|
|
52
53
|
aud: z.string().regex(/^app:[a-z][a-z0-9-]{0,79}$/),
|
|
@@ -61,6 +62,8 @@ const InvocationPayloadSchema = z
|
|
|
61
62
|
/** Only for an app session of the mobile app, which never acts as an administrator. */
|
|
62
63
|
session_kind: z.literal("app").optional(),
|
|
63
64
|
credential_id: BoundedIdentifierSchema.optional(),
|
|
65
|
+
oauth_client_id: z.string().min(1).max(200).optional(),
|
|
66
|
+
credential_expires_at: z.iso.datetime().optional(),
|
|
64
67
|
scopes: z
|
|
65
68
|
.array(ScopeSchema)
|
|
66
69
|
.max(100)
|
|
@@ -79,52 +82,66 @@ const InvocationPayloadSchema = z
|
|
|
79
82
|
nbf: z.number().int().nonnegative(),
|
|
80
83
|
exp: z.number().int().positive(),
|
|
81
84
|
})
|
|
82
|
-
.strict()
|
|
83
|
-
.superRefine((claims, context) => {
|
|
84
|
-
if (
|
|
85
|
-
(claims.parent_invocation_id
|
|
86
|
-
? claims.exp <= claims.iat ||
|
|
87
|
-
claims.exp - claims.iat > CLOUD_INVOCATION_TOKEN_TTL_SECONDS ||
|
|
88
|
-
!claims.op.startsWith("capability.query:")
|
|
89
|
-
: claims.exp - claims.iat !== CLOUD_INVOCATION_TOKEN_TTL_SECONDS) ||
|
|
90
|
-
claims.nbf !== claims.iat
|
|
91
|
-
) {
|
|
92
|
-
context.addIssue({ code: "custom", message: "Invocation lifetime is invalid" });
|
|
93
|
-
}
|
|
85
|
+
.strict();
|
|
94
86
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
context.addIssue({ code: "custom", message: "A session invocation must use a user principal" });
|
|
111
|
-
}
|
|
112
|
-
if (claims.session_kind && claims.credential_kind !== "session") {
|
|
113
|
-
context.addIssue({ code: "custom", message: "Only a session invocation has a session kind" });
|
|
87
|
+
const validateInvocationClaims = (claims: z.infer<typeof InvocationPayloadBaseSchema>, context: z.RefinementCtx, subjectProbe = false) => {
|
|
88
|
+
if (
|
|
89
|
+
(claims.parent_invocation_id
|
|
90
|
+
? claims.exp <= claims.iat ||
|
|
91
|
+
claims.exp - claims.iat > CLOUD_INVOCATION_TOKEN_TTL_SECONDS ||
|
|
92
|
+
!claims.op.startsWith("capability.query:")
|
|
93
|
+
: claims.exp - claims.iat !== CLOUD_INVOCATION_TOKEN_TTL_SECONDS) ||
|
|
94
|
+
claims.nbf !== claims.iat
|
|
95
|
+
) {
|
|
96
|
+
context.addIssue({ code: "custom", message: "Invocation lifetime is invalid" });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (claims.principal_type === "user") {
|
|
100
|
+
if (claims.access_subject_type !== "user" || claims.access_subject_id !== claims.sub || claims.delegated_user_id !== undefined) {
|
|
101
|
+
context.addIssue({ code: "custom", message: "User invocation authority is inconsistent" });
|
|
114
102
|
}
|
|
115
|
-
if (claims.credential_kind === "
|
|
116
|
-
context.addIssue({ code: "custom", message: "
|
|
103
|
+
if (claims.credential_kind === "api_key") {
|
|
104
|
+
context.addIssue({ code: "custom", message: "An API key must act through a service account" });
|
|
117
105
|
}
|
|
118
|
-
|
|
119
|
-
|
|
106
|
+
} else if (claims.delegated_user_id) {
|
|
107
|
+
if (claims.access_subject_type !== "user" || claims.access_subject_id !== claims.delegated_user_id) {
|
|
108
|
+
context.addIssue({ code: "custom", message: "Delegated invocation authority is inconsistent" });
|
|
120
109
|
}
|
|
110
|
+
} else if (claims.access_subject_type !== "service_account" || claims.access_subject_id !== claims.sub) {
|
|
111
|
+
context.addIssue({ code: "custom", message: "Service-account invocation authority is inconsistent" });
|
|
112
|
+
}
|
|
113
|
+
if (claims.credential_kind === "session" && claims.principal_type !== "user") {
|
|
114
|
+
context.addIssue({ code: "custom", message: "A session invocation must use a user principal" });
|
|
115
|
+
}
|
|
116
|
+
if (claims.session_kind && claims.credential_kind !== "session") {
|
|
117
|
+
context.addIssue({ code: "custom", message: "Only a session invocation has a session kind" });
|
|
118
|
+
}
|
|
119
|
+
if (claims.credential_kind === "session" && claims.scopes.length > 0) {
|
|
120
|
+
context.addIssue({ code: "custom", message: "A session invocation cannot carry credential scopes" });
|
|
121
|
+
}
|
|
122
|
+
if (claims.credential_kind === "api_key" && !claims.credential_id) {
|
|
123
|
+
context.addIssue({ code: "custom", message: "An API-key invocation requires credential provenance" });
|
|
124
|
+
}
|
|
121
125
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
const mandateFields = [claims.mandate_id, claims.mandate_revision, claims.workload_type, claims.workload_id];
|
|
127
|
+
const mandateFieldCount = mandateFields.filter((value) => value !== undefined).length;
|
|
128
|
+
if (
|
|
129
|
+
claims.credential_kind === "mandate"
|
|
130
|
+
? mandateFieldCount !== mandateFields.length && !(subjectProbe && mandateFieldCount === 0)
|
|
131
|
+
: mandateFieldCount !== 0
|
|
132
|
+
) {
|
|
133
|
+
context.addIssue({ code: "custom", message: "Mandate provenance is incomplete or unexpected" });
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const InvocationPayloadSchema = InvocationPayloadBaseSchema.superRefine((claims, context) => validateInvocationClaims(claims, context));
|
|
138
|
+
const AppAccessProbeSchema = InvocationPayloadBaseSchema.extend({
|
|
139
|
+
token_use: z.literal("app_access_probe"),
|
|
140
|
+
act: z.object({ sub: z.literal("app:core") }).strict(),
|
|
141
|
+
op: z.literal("app-access.check"),
|
|
142
|
+
schema_hash: z.null(),
|
|
143
|
+
parent_invocation_id: z.never().optional(),
|
|
144
|
+
}).superRefine((claims, context) => validateInvocationClaims({ ...claims, token_use: "invocation" }, context, true));
|
|
128
145
|
|
|
129
146
|
export type CloudInvocationClaims = z.infer<typeof InvocationPayloadSchema>;
|
|
130
147
|
export type InvocationAuthority = Pick<
|
|
@@ -137,6 +154,8 @@ export type InvocationAuthority = Pick<
|
|
|
137
154
|
| "credential_kind"
|
|
138
155
|
| "session_kind"
|
|
139
156
|
| "credential_id"
|
|
157
|
+
| "oauth_client_id"
|
|
158
|
+
| "credential_expires_at"
|
|
140
159
|
| "scopes"
|
|
141
160
|
| "mandate_id"
|
|
142
161
|
| "mandate_revision"
|
|
@@ -245,6 +264,26 @@ export const signInvocationToken = async (params: {
|
|
|
245
264
|
}
|
|
246
265
|
};
|
|
247
266
|
|
|
267
|
+
async function verifyWithKeyRefresh(
|
|
268
|
+
verify: (key: JWTVerifyGetKey) => ReturnType<typeof jwtVerify>,
|
|
269
|
+
key: JWTVerifyGetKey,
|
|
270
|
+
remote: { url: URL; state: RemoteSetState } | null,
|
|
271
|
+
) {
|
|
272
|
+
try {
|
|
273
|
+
return await verify(key);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (!(error instanceof errors.JWKSNoMatchingKey) || !remote) throw error;
|
|
276
|
+
const now = Date.now();
|
|
277
|
+
if (now - remote.state.lastForcedRefreshAt < 30_000) throw error;
|
|
278
|
+
remote.state.lastForcedRefreshAt = now;
|
|
279
|
+
identityMetrics.increment("unknown_kid_refresh");
|
|
280
|
+
const refreshed = createRemoteSet(remote.url);
|
|
281
|
+
const verified = await verify(refreshed);
|
|
282
|
+
remote.state.key = refreshed;
|
|
283
|
+
return verified;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
248
287
|
/** With deferSchemaBinding, the caller must compare schema_hash after resolving live authority. */
|
|
249
288
|
export const verifyInvocationToken = async (
|
|
250
289
|
token: string,
|
|
@@ -262,7 +301,7 @@ export const verifyInvocationToken = async (
|
|
|
262
301
|
const jwksUrl = options.jwksUrl ?? runtime?.invocationJwksUrl;
|
|
263
302
|
const state = options.key || !jwksUrl ? null : remoteSet(jwksUrl);
|
|
264
303
|
if (state && jwksUrl) remote = { url: jwksUrl, state };
|
|
265
|
-
|
|
304
|
+
const key = options.key ?? state?.key;
|
|
266
305
|
if (!key) throw new Error("Cloud invocation verifier has no public key source");
|
|
267
306
|
assertProtectedHeader(decodeProtectedHeader(token));
|
|
268
307
|
const verify = (candidate: JWTVerifyGetKey) =>
|
|
@@ -274,19 +313,7 @@ export const verifyInvocationToken = async (
|
|
|
274
313
|
clockTolerance: INVOCATION_CLOCK_TOLERANCE_SECONDS,
|
|
275
314
|
currentDate: options.now,
|
|
276
315
|
});
|
|
277
|
-
|
|
278
|
-
try {
|
|
279
|
-
verified = await verify(key);
|
|
280
|
-
} catch (error) {
|
|
281
|
-
if (!(error instanceof errors.JWKSNoMatchingKey) || !remote) throw error;
|
|
282
|
-
const now = Date.now();
|
|
283
|
-
if (now - remote.state.lastForcedRefreshAt < 30_000) throw error;
|
|
284
|
-
remote.state.lastForcedRefreshAt = now;
|
|
285
|
-
identityMetrics.increment("unknown_kid_refresh");
|
|
286
|
-
key = createRemoteSet(remote.url);
|
|
287
|
-
verified = await verify(key);
|
|
288
|
-
remote.state.key = key;
|
|
289
|
-
}
|
|
316
|
+
const verified = await verifyWithKeyRefresh(verify, key, remote);
|
|
290
317
|
const parsed = InvocationPayloadSchema.safeParse(verified.payload);
|
|
291
318
|
if (!parsed.success || parsed.data.op !== operation || (!options.deferSchemaBinding && parsed.data.schema_hash !== schemaHash)) {
|
|
292
319
|
throw new Error("Cloud invocation JWT claims do not match the target operation");
|
|
@@ -303,3 +330,158 @@ export const verifyInvocationToken = async (
|
|
|
303
330
|
export const clearInvocationVerifierCachesForTest = (): void => {
|
|
304
331
|
remoteSets.clear();
|
|
305
332
|
};
|
|
333
|
+
|
|
334
|
+
const AppEventInvocationClaimsSchema = z
|
|
335
|
+
.object({
|
|
336
|
+
iss: z.string().url(),
|
|
337
|
+
aud: z.string(),
|
|
338
|
+
token_use: z.literal("app_event_delivery"),
|
|
339
|
+
act: z.literal("app:core"),
|
|
340
|
+
op: z.literal("app-event.deliver"),
|
|
341
|
+
subscription_id: z.string().min(1).max(400),
|
|
342
|
+
event_id: z.uuid(),
|
|
343
|
+
payload_hash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
344
|
+
iat: z.number().int(),
|
|
345
|
+
nbf: z.number().int(),
|
|
346
|
+
exp: z.number().int(),
|
|
347
|
+
jti: z.uuid(),
|
|
348
|
+
})
|
|
349
|
+
.strict();
|
|
350
|
+
|
|
351
|
+
export const appEventPayloadHash = (payload: unknown): string => createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
352
|
+
|
|
353
|
+
/** Core-to-app invocation, bound to one subscription, event and exact request body. */
|
|
354
|
+
export const signAppEventInvocation = async (input: {
|
|
355
|
+
appId: string;
|
|
356
|
+
subscriptionId: string;
|
|
357
|
+
eventId: string;
|
|
358
|
+
payload: unknown;
|
|
359
|
+
issuer?: string;
|
|
360
|
+
signer?: PreparedIdentitySigner;
|
|
361
|
+
now?: Date;
|
|
362
|
+
jti?: string;
|
|
363
|
+
}) => {
|
|
364
|
+
const issuer = input.issuer ?? (await getIdentityRuntimeConfig()).issuer;
|
|
365
|
+
const signer = input.signer ?? (await prepareIdentitySigner("invocation"));
|
|
366
|
+
const now = Math.floor((input.now?.getTime() ?? Date.now()) / 1_000);
|
|
367
|
+
if (signer.signUntil.getTime() < (now + CLOUD_INVOCATION_TOKEN_TTL_SECONDS) * 1000)
|
|
368
|
+
throw new Error("Invocation signing key expires before the delivery token");
|
|
369
|
+
const claims = AppEventInvocationClaimsSchema.parse({
|
|
370
|
+
iss: issuer,
|
|
371
|
+
aud: `app:${AppIdSchema.parse(input.appId)}`,
|
|
372
|
+
token_use: "app_event_delivery",
|
|
373
|
+
act: "app:core",
|
|
374
|
+
op: "app-event.deliver",
|
|
375
|
+
subscription_id: input.subscriptionId,
|
|
376
|
+
event_id: input.eventId,
|
|
377
|
+
payload_hash: appEventPayloadHash(input.payload),
|
|
378
|
+
iat: now,
|
|
379
|
+
nbf: now,
|
|
380
|
+
exp: now + CLOUD_INVOCATION_TOKEN_TTL_SECONDS,
|
|
381
|
+
jti: input.jti ?? crypto.randomUUID(),
|
|
382
|
+
});
|
|
383
|
+
return new SignJWT(claims)
|
|
384
|
+
.setProtectedHeader({ alg: CLOUD_IDENTITY_ALGORITHM, typ: CLOUD_INVOCATION_TOKEN_TYPE, kid: signer.kid })
|
|
385
|
+
.sign(signer.key);
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
export const verifyAppEventInvocation = async (
|
|
389
|
+
token: string,
|
|
390
|
+
input: { appId: string; subscriptionId: string; eventId: string; payload: unknown },
|
|
391
|
+
options: { issuer?: string; key?: JWTVerifyGetKey; jwksUrl?: URL; now?: Date } = {},
|
|
392
|
+
): Promise<boolean> => {
|
|
393
|
+
if (new TextEncoder().encode(token).byteLength > IDENTITY_MAX_COMPACT_TOKEN_BYTES) return false;
|
|
394
|
+
try {
|
|
395
|
+
assertProtectedHeader(decodeProtectedHeader(token));
|
|
396
|
+
const runtime = options.issuer && (options.key || options.jwksUrl) ? null : await getIdentityRuntimeConfig();
|
|
397
|
+
const issuer = options.issuer ?? runtime!.issuer;
|
|
398
|
+
const url = options.jwksUrl ?? runtime?.invocationJwksUrl;
|
|
399
|
+
const state = options.key || !url ? null : remoteSet(url);
|
|
400
|
+
const key = options.key ?? state?.key;
|
|
401
|
+
if (!key) return false;
|
|
402
|
+
const verify = (key: JWTVerifyGetKey) =>
|
|
403
|
+
jwtVerify(token, key, {
|
|
404
|
+
algorithms: [CLOUD_IDENTITY_ALGORITHM],
|
|
405
|
+
audience: `app:${AppIdSchema.parse(input.appId)}`,
|
|
406
|
+
issuer,
|
|
407
|
+
typ: CLOUD_INVOCATION_TOKEN_TYPE,
|
|
408
|
+
clockTolerance: INVOCATION_CLOCK_TOLERANCE_SECONDS,
|
|
409
|
+
currentDate: options.now,
|
|
410
|
+
});
|
|
411
|
+
const result = await verifyWithKeyRefresh(verify, key, state && url ? { url, state } : null);
|
|
412
|
+
const claims = AppEventInvocationClaimsSchema.parse(result.payload);
|
|
413
|
+
return (
|
|
414
|
+
claims.exp - claims.iat === CLOUD_INVOCATION_TOKEN_TTL_SECONDS &&
|
|
415
|
+
claims.nbf === claims.iat &&
|
|
416
|
+
claims.subscription_id === input.subscriptionId &&
|
|
417
|
+
claims.event_id === input.eventId &&
|
|
418
|
+
claims.payload_hash === appEventPayloadHash(input.payload)
|
|
419
|
+
);
|
|
420
|
+
} catch {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
/** Core-only, target-bound existence proof. It cannot authorize an application operation. */
|
|
426
|
+
export const signAppAccessProbe = async (input: {
|
|
427
|
+
appId: string;
|
|
428
|
+
authority: InvocationAuthority;
|
|
429
|
+
issuer?: string;
|
|
430
|
+
signer?: PreparedIdentitySigner;
|
|
431
|
+
now?: Date;
|
|
432
|
+
}) => {
|
|
433
|
+
const issuer = input.issuer ?? (await getIdentityRuntimeConfig()).issuer;
|
|
434
|
+
const signer = input.signer ?? (await prepareIdentitySigner("invocation"));
|
|
435
|
+
const now = Math.floor((input.now?.getTime() ?? Date.now()) / 1_000);
|
|
436
|
+
if (signer.signUntil.getTime() < (now + CLOUD_INVOCATION_TOKEN_TTL_SECONDS) * 1000)
|
|
437
|
+
throw new Error("Invocation signing key expires before the app-access probe");
|
|
438
|
+
const claims = AppAccessProbeSchema.parse({
|
|
439
|
+
...input.authority,
|
|
440
|
+
iss: issuer,
|
|
441
|
+
aud: `app:${AppIdSchema.parse(input.appId)}`,
|
|
442
|
+
token_use: "app_access_probe",
|
|
443
|
+
act: { sub: "app:core" },
|
|
444
|
+
op: "app-access.check",
|
|
445
|
+
schema_hash: null,
|
|
446
|
+
ver: CLOUD_INVOCATION_PROTOCOL_VERSION,
|
|
447
|
+
iat: now,
|
|
448
|
+
nbf: now,
|
|
449
|
+
exp: now + CLOUD_INVOCATION_TOKEN_TTL_SECONDS,
|
|
450
|
+
jti: crypto.randomUUID(),
|
|
451
|
+
});
|
|
452
|
+
const token = await new SignJWT(claims)
|
|
453
|
+
.setProtectedHeader({ alg: CLOUD_IDENTITY_ALGORITHM, typ: CLOUD_INVOCATION_TOKEN_TYPE, kid: signer.kid })
|
|
454
|
+
.sign(signer.key);
|
|
455
|
+
if (new TextEncoder().encode(token).byteLength > IDENTITY_MAX_COMPACT_TOKEN_BYTES) throw new Error("App-access probe exceeds 4 KiB");
|
|
456
|
+
return token;
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
export const verifyAppAccessProbe = async (
|
|
460
|
+
token: string,
|
|
461
|
+
appId: string,
|
|
462
|
+
options: { issuer?: string; key?: JWTVerifyGetKey; jwksUrl?: URL; now?: Date } = {},
|
|
463
|
+
): Promise<z.infer<typeof AppAccessProbeSchema> | null> => {
|
|
464
|
+
if (new TextEncoder().encode(token).byteLength > IDENTITY_MAX_COMPACT_TOKEN_BYTES) return null;
|
|
465
|
+
try {
|
|
466
|
+
assertProtectedHeader(decodeProtectedHeader(token));
|
|
467
|
+
const runtime = options.issuer && (options.key || options.jwksUrl) ? null : await getIdentityRuntimeConfig();
|
|
468
|
+
const issuer = options.issuer ?? runtime!.issuer;
|
|
469
|
+
const url = options.jwksUrl ?? runtime?.invocationJwksUrl;
|
|
470
|
+
const state = options.key || !url ? null : remoteSet(url);
|
|
471
|
+
const key = options.key ?? state?.key;
|
|
472
|
+
if (!key) return null;
|
|
473
|
+
const verify = (key: JWTVerifyGetKey) =>
|
|
474
|
+
jwtVerify(token, key, {
|
|
475
|
+
algorithms: [CLOUD_IDENTITY_ALGORITHM],
|
|
476
|
+
audience: `app:${AppIdSchema.parse(appId)}`,
|
|
477
|
+
issuer,
|
|
478
|
+
typ: CLOUD_INVOCATION_TOKEN_TYPE,
|
|
479
|
+
clockTolerance: INVOCATION_CLOCK_TOLERANCE_SECONDS,
|
|
480
|
+
currentDate: options.now,
|
|
481
|
+
});
|
|
482
|
+
const result = await verifyWithKeyRefresh(verify, key, state && url ? { url, state } : null);
|
|
483
|
+
return AppAccessProbeSchema.parse(result.payload);
|
|
484
|
+
} catch {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
};
|
|
@@ -33,6 +33,7 @@ export const resolveIssuanceAuthority = async (
|
|
|
33
33
|
|
|
34
34
|
type RegisteredAppAccessOptions = {
|
|
35
35
|
db?: SQL;
|
|
36
|
+
signal?: AbortSignal;
|
|
36
37
|
getApp?: (id: string) => Promise<Pick<AppMeta, "id" | "access"> | null>;
|
|
37
38
|
};
|
|
38
39
|
|
|
@@ -41,8 +42,10 @@ export const requireRegisteredAppAccess = async (
|
|
|
41
42
|
appId: string,
|
|
42
43
|
options: RegisteredAppAccessOptions = {},
|
|
43
44
|
): Promise<void> => {
|
|
45
|
+
options.signal?.throwIfAborted();
|
|
44
46
|
const app = await (options.getApp ?? getApp)(appId).catch(() => null);
|
|
45
|
-
if (!app || !authority || !(await canUseApp(authority, app, { db: options.db }))) {
|
|
47
|
+
if (!app || !authority || !(await canUseApp(authority, app, { db: options.db, signal: options.signal }))) {
|
|
48
|
+
options.signal?.throwIfAborted();
|
|
46
49
|
if (app) recordAppAccessDenied(app.id);
|
|
47
50
|
throw new AppAccessDeniedError(app ?? { id: appId });
|
|
48
51
|
}
|
|
@@ -57,6 +60,7 @@ export const issueInvocationToken = async (
|
|
|
57
60
|
sign?: typeof signInvocationToken;
|
|
58
61
|
} = {},
|
|
59
62
|
): ReturnType<typeof signInvocationToken> => {
|
|
63
|
+
dependencies.signal?.throwIfAborted();
|
|
60
64
|
const authority = await (dependencies.resolve ?? resolveIssuanceAuthority)(
|
|
61
65
|
params.authority,
|
|
62
66
|
params.callingAppId,
|
|
@@ -66,5 +70,6 @@ export const issueInvocationToken = async (
|
|
|
66
70
|
if (params.parent && (!authority || !matchesInvocationSubject(params.parent, authority)))
|
|
67
71
|
throw new AppAccessDeniedError({ id: params.targetAppId });
|
|
68
72
|
await requireRegisteredAppAccess(authority, params.targetAppId, dependencies);
|
|
73
|
+
dependencies.signal?.throwIfAborted();
|
|
69
74
|
return (dependencies.sign ?? signInvocationToken)(params);
|
|
70
75
|
};
|
|
@@ -366,12 +366,76 @@ const stripWebp = (bytes: Uint8Array): Uint8Array => {
|
|
|
366
366
|
return out.subarray(0, written);
|
|
367
367
|
};
|
|
368
368
|
|
|
369
|
+
const stripGif = (bytes: Uint8Array): Uint8Array => {
|
|
370
|
+
const bad = () => new ImageMetadataError("GIF");
|
|
371
|
+
if (bytes.length < 13) throw bad();
|
|
372
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
373
|
+
if (!view.getUint16(6, true) || !view.getUint16(8, true)) throw bad();
|
|
374
|
+
let offset = 13 + (bytes[10]! & 0x80 ? 3 * (1 << ((bytes[10]! & 7) + 1)) : 0);
|
|
375
|
+
if (offset > bytes.length) throw bad();
|
|
376
|
+
const out = new Uint8Array(bytes.length);
|
|
377
|
+
let written = copy(out, 0, bytes, 0, offset);
|
|
378
|
+
let image = false;
|
|
379
|
+
const blocksEnd = (start: number) => {
|
|
380
|
+
let next = start;
|
|
381
|
+
while (next < bytes.length) {
|
|
382
|
+
const size = bytes[next++]!;
|
|
383
|
+
if (!size) return next;
|
|
384
|
+
if (next + size > bytes.length) throw bad();
|
|
385
|
+
next += size;
|
|
386
|
+
}
|
|
387
|
+
throw bad();
|
|
388
|
+
};
|
|
389
|
+
while (offset < bytes.length) {
|
|
390
|
+
const start = offset;
|
|
391
|
+
const type = bytes[offset++]!;
|
|
392
|
+
if (type === 0x3b) {
|
|
393
|
+
if (!image) throw bad();
|
|
394
|
+
out[written++] = type;
|
|
395
|
+
return out.subarray(0, written);
|
|
396
|
+
}
|
|
397
|
+
if (type === 0x2c) {
|
|
398
|
+
if (offset + 9 > bytes.length || !view.getUint16(offset + 4, true) || !view.getUint16(offset + 6, true)) throw bad();
|
|
399
|
+
const packed = bytes[offset + 8]!;
|
|
400
|
+
offset += 9 + (packed & 0x80 ? 3 * (1 << ((packed & 7) + 1)) : 0);
|
|
401
|
+
if (offset >= bytes.length || bytes[offset]! < 2 || bytes[offset]! > 8) throw bad();
|
|
402
|
+
offset = blocksEnd(offset + 1);
|
|
403
|
+
written = copy(out, written, bytes, start, offset);
|
|
404
|
+
image = true;
|
|
405
|
+
} else if (type === 0x21) {
|
|
406
|
+
if (offset >= bytes.length) throw bad();
|
|
407
|
+
const label = bytes[offset++]!;
|
|
408
|
+
const payload = offset;
|
|
409
|
+
offset = blocksEnd(offset);
|
|
410
|
+
if (label === 0xf9) {
|
|
411
|
+
if (bytes[payload] !== 4 || offset !== payload + 6) throw bad();
|
|
412
|
+
written = copy(out, written, bytes, start, offset);
|
|
413
|
+
} else if (label === 0x01) {
|
|
414
|
+
// Plain text extensions contribute visible content, unlike comments and private application blocks.
|
|
415
|
+
if (bytes[payload] !== 12) throw bad();
|
|
416
|
+
written = copy(out, written, bytes, start, offset);
|
|
417
|
+
} else if (
|
|
418
|
+
label === 0xff &&
|
|
419
|
+
bytes[payload] === 11 &&
|
|
420
|
+
offset === start + 19 &&
|
|
421
|
+
(startsWith(bytes, "NETSCAPE2.0", payload + 1) || startsWith(bytes, "ANIMEXTS1.0", payload + 1)) &&
|
|
422
|
+
bytes[payload + 12] === 3 &&
|
|
423
|
+
bytes[payload + 13] === 1
|
|
424
|
+
) {
|
|
425
|
+
written = copy(out, written, bytes, start, offset);
|
|
426
|
+
}
|
|
427
|
+
} else throw bad();
|
|
428
|
+
}
|
|
429
|
+
throw bad();
|
|
430
|
+
};
|
|
431
|
+
|
|
369
432
|
/**
|
|
370
|
-
* Losslessly removes non-rendering metadata from JPEG, PNG and
|
|
433
|
+
* Losslessly removes non-rendering metadata from JPEG, PNG, WebP and GIF, selected
|
|
371
434
|
* by magic bytes. Call only for uploads the application classifies as images.
|
|
372
435
|
* Other formats are returned unchanged. No decoding; linear time and space.
|
|
373
436
|
*/
|
|
374
437
|
export const stripImageMetadata = (bytes: Uint8Array): Uint8Array => {
|
|
438
|
+
if (startsWith(bytes, "GIF87a") || startsWith(bytes, "GIF89a")) return stripGif(bytes);
|
|
375
439
|
if (bytes[0] === 0xff && bytes[1] === 0xd8) return stripJpeg(bytes);
|
|
376
440
|
if (startsWith(bytes, "\x89PNG")) return stripPng(bytes);
|
|
377
441
|
if (startsWith(bytes, "RIFF") && startsWith(bytes, "WEBP", 8)) return stripWebp(bytes);
|
package/src/services/index.ts
CHANGED
|
@@ -104,7 +104,7 @@ export type {
|
|
|
104
104
|
ServiceAccountCredentialOwner,
|
|
105
105
|
ServiceAccountCredentialStatus,
|
|
106
106
|
} from "./service-account-credentials";
|
|
107
|
-
export { oauthTokens } from "./oauth-tokens";
|
|
107
|
+
export { isServiceAccountOAuthAuthorityCurrent, oauthTokens } from "./oauth-tokens";
|
|
108
108
|
export type { AuthenticatedOAuthToken } from "./oauth-tokens";
|
|
109
109
|
export { webauthn } from "./webauthn";
|
|
110
110
|
export type { WebAuthnRp } from "./webauthn";
|
|
@@ -251,6 +251,7 @@ export {
|
|
|
251
251
|
export type { NetworkLookup, PublicNetworkAddress } from "./network-security";
|
|
252
252
|
|
|
253
253
|
export { geoip } from "./geoip";
|
|
254
|
+
export { appDetails } from "./app-details";
|
|
254
255
|
export {
|
|
255
256
|
knownBrowsers,
|
|
256
257
|
KNOWN_BROWSER_LIMITS,
|
|
@@ -279,3 +280,14 @@ export {
|
|
|
279
280
|
requireAppAccessManager,
|
|
280
281
|
type AppAccessSnapshot,
|
|
281
282
|
} from "./app-access-management";
|
|
283
|
+
|
|
284
|
+
export { storage } from "./object-store";
|
|
285
|
+
export type {
|
|
286
|
+
ObjectStoreFile,
|
|
287
|
+
ObjectVisibility,
|
|
288
|
+
ObjectWriteData,
|
|
289
|
+
ObjectWriteOptions,
|
|
290
|
+
ObjectPresignOptions,
|
|
291
|
+
ObjectListOptions,
|
|
292
|
+
ObjectListResult,
|
|
293
|
+
} from "./object-store";
|
package/src/services/ipa/sync.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sql } from "bun";
|
|
2
|
+
import { coreUserEventPublisher } from "../../events/core-user-created";
|
|
2
3
|
import { freeipa } from "../../server/services";
|
|
3
4
|
import { writeDeletedAccountAudit } from "../account-lifecycle/audit";
|
|
4
5
|
import { calculateIpaProfileFromGroupNames, parseIpaAccountTransitionPolicy, parseIpaMatchMode } from "../account-model";
|
|
@@ -461,8 +462,11 @@ export const syncFromIpa = async (runtime: IpaSyncRuntime = {}): Promise<void> =
|
|
|
461
462
|
RETURNING id, (xmax = 0) AS inserted`;
|
|
462
463
|
await upsertUserIpaData(tx, { userId: upserted[0]!.id as string, ...u });
|
|
463
464
|
upsertedUsersByUid += 1;
|
|
464
|
-
if (Boolean(upserted[0]?.inserted))
|
|
465
|
-
|
|
465
|
+
if (Boolean(upserted[0]?.inserted)) {
|
|
466
|
+
insertedUsersByUid += 1;
|
|
467
|
+
const userId = upserted[0]!.id as string;
|
|
468
|
+
await coreUserEventPublisher().publish(tx, "user.created", { subject: userId, data: { userId, provider: "ipa" } });
|
|
469
|
+
} else updatedUsersByUid += 1;
|
|
466
470
|
}
|
|
467
471
|
|
|
468
472
|
for (let staleIndex = 0; staleIndex < staleLocalUsers.length; staleIndex += 1) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { password } from "@k2b/stdlib";
|
|
2
2
|
import { sql } from "bun";
|
|
3
3
|
import type { MutationResult, UserProfile } from "../../contracts/shared";
|
|
4
|
+
import { coreUserEventPublisher } from "../../events/core-user-created";
|
|
4
5
|
import { freeipa } from "../../server/services";
|
|
5
6
|
import { writeDeletedAccountAudit } from "../account-lifecycle/audit";
|
|
6
7
|
import { resolveProviderProfile } from "../accounts/base-user";
|
|
@@ -366,7 +367,7 @@ export const addIpa = async (params: {
|
|
|
366
367
|
display_name = EXCLUDED.display_name,
|
|
367
368
|
mail = EXCLUDED.mail,
|
|
368
369
|
account_expires = EXCLUDED.account_expires
|
|
369
|
-
RETURNING id
|
|
370
|
+
RETURNING id, (xmax = 0) AS inserted
|
|
370
371
|
`;
|
|
371
372
|
id = insertRows[0]!.id as string;
|
|
372
373
|
await upsertUserIpaData(
|
|
@@ -381,6 +382,8 @@ export const addIpa = async (params: {
|
|
|
381
382
|
},
|
|
382
383
|
tx,
|
|
383
384
|
);
|
|
385
|
+
if (insertRows[0]?.inserted)
|
|
386
|
+
await coreUserEventPublisher().publish(tx, "user.created", { subject: id, data: { userId: id, provider: "ipa" } });
|
|
384
387
|
}
|
|
385
388
|
} catch (dbError) {
|
|
386
389
|
log.error("CRITICAL: FreeIPA user created but local DB update failed. Manual reconciliation needed.", {
|
|
@@ -194,6 +194,11 @@ export const isMandatePolicyNarrowing = (current: MandatePolicyV1, next: Mandate
|
|
|
194
194
|
}),
|
|
195
195
|
));
|
|
196
196
|
|
|
197
|
+
/**
|
|
198
|
+
* True when a background mandate permits one operation. Unattended Actions must fit the live
|
|
199
|
+
* capability policy whenever the mandate has grants or preapproves Actions; a `require_approval`
|
|
200
|
+
* mandate without grants runs an Action only after its separate approval completed through Cloud.
|
|
201
|
+
*/
|
|
197
202
|
export const mandatePolicyAllows = (
|
|
198
203
|
policy: MandatePolicyV1,
|
|
199
204
|
input: {
|
|
@@ -224,14 +229,14 @@ export const mandatePolicyAllows = (
|
|
|
224
229
|
)
|
|
225
230
|
);
|
|
226
231
|
}
|
|
232
|
+
const [operation, capabilityId] = input.operation.split(":");
|
|
233
|
+
const kind =
|
|
234
|
+
operation === "capability.query"
|
|
235
|
+
? "query"
|
|
236
|
+
: operation === "capability.action.run" || operation === "capability.action.review"
|
|
237
|
+
? "action"
|
|
238
|
+
: null;
|
|
227
239
|
if (policy.grants !== undefined) {
|
|
228
|
-
const [operation, capabilityId] = input.operation.split(":");
|
|
229
|
-
const kind =
|
|
230
|
-
operation === "capability.query"
|
|
231
|
-
? "query"
|
|
232
|
-
: operation === "capability.action.run" || operation === "capability.action.review"
|
|
233
|
-
? "action"
|
|
234
|
-
: null;
|
|
235
240
|
if (
|
|
236
241
|
!kind ||
|
|
237
242
|
!capabilityId ||
|
|
@@ -240,11 +245,13 @@ export const mandatePolicyAllows = (
|
|
|
240
245
|
)
|
|
241
246
|
)
|
|
242
247
|
return false;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
+
}
|
|
249
|
+
// Routine grants and unattended preapproval must both fit the live capability policy.
|
|
250
|
+
// A no-grants require_approval mandate keeps its separately approved execution path.
|
|
251
|
+
if (kind === "action" && (policy.grants !== undefined || policy.actions === "preapproved")) {
|
|
252
|
+
if (input.capabilityPolicy) {
|
|
253
|
+
if (!("silent" in input.capabilityPolicy) || input.capabilityPolicy.routines === false) return false;
|
|
254
|
+
} else if (input.capabilityApproval !== "none" && input.capabilityApproval !== "rememberable") return false;
|
|
248
255
|
}
|
|
249
256
|
if (!input.operation.startsWith("capability.action.run:")) return true;
|
|
250
257
|
if (policy.actions === "deny") return false;
|
|
@@ -29,6 +29,7 @@ type BrowserDeliveryPayload = {
|
|
|
29
29
|
group?: string;
|
|
30
30
|
badge?: number;
|
|
31
31
|
createdAt?: number;
|
|
32
|
+
alert?: false;
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
// RFC 8030 section 7.2 guarantees 4,096 bytes; aes128gcm adds a header, tag, and padding delimiter.
|
|
@@ -44,6 +45,7 @@ const browserPushMessage = (payload: BrowserDeliveryPayload): string =>
|
|
|
44
45
|
...(payload.group !== undefined ? { group: payload.group } : {}),
|
|
45
46
|
...(payload.badge !== undefined ? { badge: payload.badge } : {}),
|
|
46
47
|
...(payload.createdAt !== undefined ? { createdAt: payload.createdAt } : {}),
|
|
48
|
+
...(payload.alert === false ? { alert: false } : {}),
|
|
47
49
|
});
|
|
48
50
|
|
|
49
51
|
const BrowserDeliveryPayloadSchema = z.object({
|
|
@@ -59,6 +61,7 @@ const BrowserDeliveryPayloadSchema = z.object({
|
|
|
59
61
|
.optional(),
|
|
60
62
|
badge: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
|
|
61
63
|
createdAt: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
|
|
64
|
+
alert: z.literal(false).optional(),
|
|
62
65
|
});
|
|
63
66
|
|
|
64
67
|
const BrowserDestinationContextSchema = z.object({
|
|
@@ -192,6 +195,7 @@ const browserDriver = {
|
|
|
192
195
|
...(group !== undefined ? { group } : {}),
|
|
193
196
|
...(presentation.badge !== undefined ? { badge: presentation.badge } : {}),
|
|
194
197
|
...(presentation.group !== undefined || presentation.badge !== undefined ? { createdAt: Date.now() } : {}),
|
|
198
|
+
...(presentation.alert === false ? { alert: false } : {}),
|
|
195
199
|
});
|
|
196
200
|
if (payload.preview !== undefined && Buffer.byteLength(browserPushMessage(payload), "utf8") > WEB_PUSH_PLAINTEXT_BUDGET) {
|
|
197
201
|
const { preview: _preview, ...titleOnlyPayload } = payload;
|
|
@@ -17,15 +17,16 @@ const isSchemaUnavailable = (error: unknown): boolean => {
|
|
|
17
17
|
const upsertDefinition = async (definition: AnyBoundNotificationDefinition, db: typeof sql = sql): Promise<void> => {
|
|
18
18
|
const recommended = toPgTextArray([...(definition.delivery?.recommended ?? [])]);
|
|
19
19
|
const required = toPgTextArray([...(definition.delivery?.required ?? [])]);
|
|
20
|
+
const excluded = toPgTextArray([...(definition.delivery?.excluded ?? [])]);
|
|
20
21
|
await db`
|
|
21
22
|
INSERT INTO notifications.definitions (
|
|
22
23
|
id, app_id, kind, label, description, presentation, recipient_kind,
|
|
23
|
-
recommended_channels, required_channels, active, last_seen_at, updated_at
|
|
24
|
+
recommended_channels, required_channels, excluded_channels, excluded_reason, active, last_seen_at, updated_at
|
|
24
25
|
) VALUES (
|
|
25
26
|
${definition.id}, ${definition.appId}, ${definition.key}, ${definition.label},
|
|
26
27
|
${definition.description}, (${definition.presentation ? JSON.stringify(definition.presentation) : null}::text)::jsonb,
|
|
27
28
|
${definition.recipient}, ${recommended}::text[],
|
|
28
|
-
${required}::text[], true, now(), now()
|
|
29
|
+
${required}::text[], ${excluded}::text[], ${definition.excludedReason ?? null}, true, now(), now()
|
|
29
30
|
)
|
|
30
31
|
ON CONFLICT (id) DO UPDATE SET
|
|
31
32
|
app_id = EXCLUDED.app_id,
|
|
@@ -36,6 +37,8 @@ const upsertDefinition = async (definition: AnyBoundNotificationDefinition, db:
|
|
|
36
37
|
recipient_kind = EXCLUDED.recipient_kind,
|
|
37
38
|
recommended_channels = EXCLUDED.recommended_channels,
|
|
38
39
|
required_channels = EXCLUDED.required_channels,
|
|
40
|
+
excluded_channels = EXCLUDED.excluded_channels,
|
|
41
|
+
excluded_reason = EXCLUDED.excluded_reason,
|
|
39
42
|
active = true,
|
|
40
43
|
last_seen_at = now(),
|
|
41
44
|
updated_at = now()
|