@whooshbang/contract-mock 1.0.0-rc.30 → 1.0.0-rc.32
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/CHANGELOG.md +18 -0
- package/dist/core.js +53 -13
- package/dist/core.js.map +1 -1
- package/dist/types.d.ts +6 -5
- package/package.json +3 -3
- package/scenarios/v1/manifest.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ recorded here.
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 1.0.0-rc.32
|
|
9
|
+
|
|
10
|
+
- Serve the project and environment recipient-experience routes, and the
|
|
11
|
+
recipient-copy override routes, to a project credential carrying
|
|
12
|
+
`projects:read` / `projects:write` as well as to a human session and a grant.
|
|
13
|
+
The environment pair previously accepted only a session, so an integrator
|
|
14
|
+
journey run against the mock could not configure the project it was
|
|
15
|
+
integrating any more than the real service could.
|
|
16
|
+
- Reissue the mock against `@whooshbang/contracts@1.0.0-rc.20` and
|
|
17
|
+
`@whooshbang/sdk@1.0.0-rc.29`.
|
|
18
|
+
|
|
19
|
+
## 1.0.0-rc.31
|
|
20
|
+
|
|
21
|
+
- Default an unconfigured recipient experience to the canonical WhooshBang red
|
|
22
|
+
while continuing to preserve configured customer accent colours.
|
|
23
|
+
- Reissue the mock against `@whooshbang/contracts@1.0.0-rc.19` and
|
|
24
|
+
`@whooshbang/sdk@1.0.0-rc.28` for the WhooshBang-owned recipient attribution.
|
|
25
|
+
|
|
8
26
|
## 1.0.0-rc.30
|
|
9
27
|
|
|
10
28
|
- Add normalized recipient-logo upload, atomic replacement, session-authorized
|
package/dist/core.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import { canonicalIanaTimeZone, classifyWebhookDeliveryOutcome, isCustomizableRecipientCopyKey, recipientCopyOverrideIssues, RECIPIENT_BRAND_LOGO_MAX_INPUT_BYTES, RecipientBrandLogoError, normalizeRecipientBrandLogo, NOTIFICATION_IMAGE_MAX_BYTES, NotificationImageError, normalizeNotificationImage, contractValidationErrors, MAX_WEBHOOK_DELIVERY_ATTEMPTS, validateAccountProjection, validateApiCredential, validateApiCredentialCollection, validateApiCredentialRotation, validateApiCredentialSecretReveal, validateAttachNotifierConnectionRequest, validateChannelConnection, validateChannelConnectionCollection, validateConnectionProviderCollection, validateCreateGroupBroadcastRequest, validateCreateApiCredentialRequest, validateCreateChannelConnectionRequest, validateCreateCustomerEndpointRequest, validateCreateMachineClientRequest, validateCreateMessageRequest, validateCreateProjectRequest, validateCreateProviderHandoffRequest, validateCreateRecipientGroupRequest, validateCreateRecipientSessionRequest, validateCreateRecipientSessionResponse, validateCreateSubscriberRequest, validateCreateSubscriptionLinkRequest, validateCustomerEndpoint, validateCustomerEndpointAttemptCollection, validateCustomerEndpointCollection, validateCustomerEndpointMutationResult, validateInteractionEvent, validateInteractionSummary, validateMachineClient, validateMachineCredential, validateMachineEventAckRequest, validateMachineEventAckResponse, validateMachineEventPollResponse, validateMessage, validateNotifierConnectionCollection, validateWhooshBangEvent, validateProblem, validateProject, validateProjectCollection, validateProjectRecipientExperience, validateRecipientBrandLogoUpload, validateNotificationImageAsset, validateEnvironmentRecipientExperience, validateGroupBroadcast, validatePreferredBinding, validateProviderHandoff, validateRecipientGroup, validateRecipientGroupCollection, validateRecipientGroupMembership, validateRecipientGroupMembershipCollection, validateRecipientHandoffCompletion, validateRecipientCopyOverrideCollection, validateRecipientCopyOverride, validateRecipientSession, validateSetPreferredBindingRequest, validateSubscriber, validateProviderCapabilitiesCollection, validateRegisterMachineCredentialRequest, validateRotateApiCredentialRequest, validateRotateChannelConnectionCredentialRequest, validateRotateCustomerEndpointRequest, validateSubscriptionLink, validateUpdateChannelConnectionRequest, validateUpdateEnvironmentRecipientExperienceRequest, validateUpdateProjectRecipientExperienceRequest, validateUpdateRecipientCopyOverrideRequest, validateUpdateRecipientGroupRequest, verifyMachineCredentialToken, } from "@whooshbang/contracts";
|
|
2
|
+
import { canonicalIanaTimeZone, classifyWebhookDeliveryOutcome, isCustomizableRecipientCopyKey, recipientCopyOverrideIssues, RECIPIENT_BRAND_DEFAULT_ACCENT_COLOR, RECIPIENT_BRAND_LOGO_MAX_INPUT_BYTES, RecipientBrandLogoError, normalizeRecipientBrandLogo, NOTIFICATION_IMAGE_MAX_BYTES, NotificationImageError, normalizeNotificationImage, contractValidationErrors, MAX_WEBHOOK_DELIVERY_ATTEMPTS, validateAccountProjection, validateApiCredential, validateApiCredentialCollection, validateApiCredentialRotation, validateApiCredentialSecretReveal, validateAttachNotifierConnectionRequest, validateChannelConnection, validateChannelConnectionCollection, validateConnectionProviderCollection, validateCreateGroupBroadcastRequest, validateCreateApiCredentialRequest, validateCreateChannelConnectionRequest, validateCreateCustomerEndpointRequest, validateCreateMachineClientRequest, validateCreateMessageRequest, validateCreateProjectRequest, validateCreateProviderHandoffRequest, validateCreateRecipientGroupRequest, validateCreateRecipientSessionRequest, validateCreateRecipientSessionResponse, validateCreateSubscriberRequest, validateCreateSubscriptionLinkRequest, validateCustomerEndpoint, validateCustomerEndpointAttemptCollection, validateCustomerEndpointCollection, validateCustomerEndpointMutationResult, validateInteractionEvent, validateInteractionSummary, validateMachineClient, validateMachineCredential, validateMachineEventAckRequest, validateMachineEventAckResponse, validateMachineEventPollResponse, validateMessage, validateNotifierConnectionCollection, validateWhooshBangEvent, validateProblem, validateProject, validateProjectCollection, validateProjectRecipientExperience, validateRecipientBrandLogoUpload, validateNotificationImageAsset, validateEnvironmentRecipientExperience, validateGroupBroadcast, validatePreferredBinding, validateProviderHandoff, validateRecipientGroup, validateRecipientGroupCollection, validateRecipientGroupMembership, validateRecipientGroupMembershipCollection, validateRecipientHandoffCompletion, validateRecipientCopyOverrideCollection, validateRecipientCopyOverride, validateRecipientSession, validateSetPreferredBindingRequest, validateSubscriber, validateProviderCapabilitiesCollection, validateRegisterMachineCredentialRequest, validateRotateApiCredentialRequest, validateRotateChannelConnectionCredentialRequest, validateRotateCustomerEndpointRequest, validateSubscriptionLink, validateUpdateChannelConnectionRequest, validateUpdateEnvironmentRecipientExperienceRequest, validateUpdateProjectRecipientExperienceRequest, validateUpdateRecipientCopyOverrideRequest, validateUpdateRecipientGroupRequest, verifyMachineCredentialToken, } from "@whooshbang/contracts";
|
|
3
3
|
import { getScenarioDefinition } from "./scenarios.js";
|
|
4
4
|
import { parseInteractionControlInput } from "./interaction-control.js";
|
|
5
|
-
const CONTRACT_VERSION = "1.0.0-rc.
|
|
5
|
+
const CONTRACT_VERSION = "1.0.0-rc.20";
|
|
6
6
|
const SCENARIO_VERSION = "contract-mock-scenarios.v1";
|
|
7
7
|
const MAX_REQUEST_BYTES = 64 * 1024;
|
|
8
8
|
const MAX_PROVIDER_ATTEMPTS = 8;
|
|
@@ -146,7 +146,7 @@ const RECIPIENT_COPY_MAXIMUM_LENGTH_BY_KEY = {
|
|
|
146
146
|
function defaultProjectRecipientExperience(state, project) {
|
|
147
147
|
return {
|
|
148
148
|
brand: {
|
|
149
|
-
accent_color:
|
|
149
|
+
accent_color: RECIPIENT_BRAND_DEFAULT_ACCENT_COLOR,
|
|
150
150
|
color_scheme: "system",
|
|
151
151
|
display_name: project.name,
|
|
152
152
|
},
|
|
@@ -1256,8 +1256,13 @@ function authenticate(request, state, acceptedKind) {
|
|
|
1256
1256
|
(acceptedKind === "account" &&
|
|
1257
1257
|
credential.kind !== "human" &&
|
|
1258
1258
|
credential.kind !== "grant") ||
|
|
1259
|
+
(acceptedKind === "configuration" &&
|
|
1260
|
+
credential.kind !== "human" &&
|
|
1261
|
+
credential.kind !== "grant" &&
|
|
1262
|
+
credential.kind !== "project") ||
|
|
1259
1263
|
(acceptedKind !== "either" &&
|
|
1260
1264
|
acceptedKind !== "account" &&
|
|
1265
|
+
acceptedKind !== "configuration" &&
|
|
1261
1266
|
credential.kind !== acceptedKind)) {
|
|
1262
1267
|
return {
|
|
1263
1268
|
response: problemResponse(problem("credential_invalid", 401, state.idGenerator("diagnostic"))),
|
|
@@ -1294,6 +1299,41 @@ function requireHumanSession(request, state) {
|
|
|
1294
1299
|
* A project credential is refused structurally rather than by a scope check —
|
|
1295
1300
|
* it has no account-level scope to hold, because it lives inside one project.
|
|
1296
1301
|
*/
|
|
1302
|
+
/**
|
|
1303
|
+
* The authority that may read or write one project's configuration.
|
|
1304
|
+
*
|
|
1305
|
+
* A person and a grant name the whole account, so the project is checked
|
|
1306
|
+
* against their membership. A project credential names exactly one project, so
|
|
1307
|
+
* a path naming another is answered the way a project that does not exist is
|
|
1308
|
+
* answered — a credential must not be able to map the account it lives in.
|
|
1309
|
+
*
|
|
1310
|
+
* This used to be a human session alone, which meant an application holding a
|
|
1311
|
+
* credential could not configure the project it was integrating (FXO-2150).
|
|
1312
|
+
*/
|
|
1313
|
+
function requireConfigurationAuthority(request, state, projectId, scope) {
|
|
1314
|
+
const result = authenticate(request, state, "configuration");
|
|
1315
|
+
if (isAuthFailure(result)) {
|
|
1316
|
+
return result;
|
|
1317
|
+
}
|
|
1318
|
+
const credential = result.credential;
|
|
1319
|
+
if (credential.kind === "human") {
|
|
1320
|
+
return { credential, requestId: result.requestId };
|
|
1321
|
+
}
|
|
1322
|
+
if (credential.kind !== "grant" && credential.kind !== "project") {
|
|
1323
|
+
return {
|
|
1324
|
+
response: problemResponse(problem("credential_invalid", 401, state.idGenerator("diagnostic"))),
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
if (!credential.scopes.includes(scope)) {
|
|
1328
|
+
return {
|
|
1329
|
+
response: problemResponse(problem("scope_forbidden", 403, state.idGenerator("diagnostic"))),
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
if (credential.kind === "project" && credential.projectId !== projectId) {
|
|
1333
|
+
return { response: notFound(state) };
|
|
1334
|
+
}
|
|
1335
|
+
return { credential, requestId: result.requestId };
|
|
1336
|
+
}
|
|
1297
1337
|
function requireAccountAuthority(request, state, scope) {
|
|
1298
1338
|
const result = authenticate(request, state, "account");
|
|
1299
1339
|
if (isAuthFailure(result)) {
|
|
@@ -3386,7 +3426,7 @@ function addPublicRoutes(app, getState, options) {
|
|
|
3386
3426
|
});
|
|
3387
3427
|
app.get("/v1/projects/:projectId/recipient-copy-overrides", (context) => {
|
|
3388
3428
|
const state = getState();
|
|
3389
|
-
const identity =
|
|
3429
|
+
const identity = requireConfigurationAuthority(context.req.raw, state, context.req.param("projectId"), "projects:read");
|
|
3390
3430
|
if (isAuthFailure(identity))
|
|
3391
3431
|
return identity.response;
|
|
3392
3432
|
const projectId = context.req.param("projectId");
|
|
@@ -3425,7 +3465,7 @@ function addPublicRoutes(app, getState, options) {
|
|
|
3425
3465
|
});
|
|
3426
3466
|
app.put("/v1/projects/:projectId/recipient-copy-overrides/:language/:copyKey", async (context) => {
|
|
3427
3467
|
const state = getState();
|
|
3428
|
-
const identity =
|
|
3468
|
+
const identity = requireConfigurationAuthority(context.req.raw, state, context.req.param("projectId"), "projects:write");
|
|
3429
3469
|
if (isAuthFailure(identity))
|
|
3430
3470
|
return identity.response;
|
|
3431
3471
|
const projectId = context.req.param("projectId");
|
|
@@ -3465,7 +3505,7 @@ function addPublicRoutes(app, getState, options) {
|
|
|
3465
3505
|
});
|
|
3466
3506
|
app.delete("/v1/projects/:projectId/recipient-copy-overrides/:language/:copyKey", (context) => {
|
|
3467
3507
|
const state = getState();
|
|
3468
|
-
const identity =
|
|
3508
|
+
const identity = requireConfigurationAuthority(context.req.raw, state, context.req.param("projectId"), "projects:write");
|
|
3469
3509
|
if (isAuthFailure(identity))
|
|
3470
3510
|
return identity.response;
|
|
3471
3511
|
const projectId = context.req.param("projectId");
|
|
@@ -3488,7 +3528,7 @@ function addPublicRoutes(app, getState, options) {
|
|
|
3488
3528
|
});
|
|
3489
3529
|
app.get("/v1/projects/:projectId/recipient-experience", (context) => {
|
|
3490
3530
|
const state = getState();
|
|
3491
|
-
const identity =
|
|
3531
|
+
const identity = requireConfigurationAuthority(context.req.raw, state, context.req.param("projectId"), "projects:read");
|
|
3492
3532
|
if (isAuthFailure(identity))
|
|
3493
3533
|
return identity.response;
|
|
3494
3534
|
const projectId = context.req.param("projectId");
|
|
@@ -3506,7 +3546,7 @@ function addPublicRoutes(app, getState, options) {
|
|
|
3506
3546
|
});
|
|
3507
3547
|
app.put("/v1/projects/:projectId/recipient-experience", async (context) => {
|
|
3508
3548
|
const state = getState();
|
|
3509
|
-
const identity =
|
|
3549
|
+
const identity = requireConfigurationAuthority(context.req.raw, state, context.req.param("projectId"), "projects:write");
|
|
3510
3550
|
if (isAuthFailure(identity))
|
|
3511
3551
|
return identity.response;
|
|
3512
3552
|
const projectId = context.req.param("projectId");
|
|
@@ -3559,7 +3599,7 @@ function addPublicRoutes(app, getState, options) {
|
|
|
3559
3599
|
});
|
|
3560
3600
|
app.put("/v1/projects/:projectId/recipient-experience/logo", async (context) => {
|
|
3561
3601
|
const state = getState();
|
|
3562
|
-
const identity =
|
|
3602
|
+
const identity = requireConfigurationAuthority(context.req.raw, state, context.req.param("projectId"), "projects:write");
|
|
3563
3603
|
if (isAuthFailure(identity))
|
|
3564
3604
|
return identity.response;
|
|
3565
3605
|
const projectId = context.req.param("projectId");
|
|
@@ -3627,10 +3667,10 @@ function addPublicRoutes(app, getState, options) {
|
|
|
3627
3667
|
});
|
|
3628
3668
|
app.get("/v1/projects/:projectId/environments/:environmentId/recipient-experience", (context) => {
|
|
3629
3669
|
const state = getState();
|
|
3630
|
-
const
|
|
3670
|
+
const projectId = context.req.param("projectId");
|
|
3671
|
+
const identity = requireConfigurationAuthority(context.req.raw, state, projectId, "projects:read");
|
|
3631
3672
|
if (isAuthFailure(identity))
|
|
3632
3673
|
return identity.response;
|
|
3633
|
-
const projectId = context.req.param("projectId");
|
|
3634
3674
|
const environment = projectEnvironmentForHuman(state, identity, projectId, context.req.param("environmentId"));
|
|
3635
3675
|
const experience = environment === undefined
|
|
3636
3676
|
? undefined
|
|
@@ -3645,10 +3685,10 @@ function addPublicRoutes(app, getState, options) {
|
|
|
3645
3685
|
});
|
|
3646
3686
|
app.put("/v1/projects/:projectId/environments/:environmentId/recipient-experience", async (context) => {
|
|
3647
3687
|
const state = getState();
|
|
3648
|
-
const
|
|
3688
|
+
const projectId = context.req.param("projectId");
|
|
3689
|
+
const identity = requireConfigurationAuthority(context.req.raw, state, projectId, "projects:write");
|
|
3649
3690
|
if (isAuthFailure(identity))
|
|
3650
3691
|
return identity.response;
|
|
3651
|
-
const projectId = context.req.param("projectId");
|
|
3652
3692
|
const environment = projectEnvironmentForHuman(state, identity, projectId, context.req.param("environmentId"));
|
|
3653
3693
|
const key = environment === undefined
|
|
3654
3694
|
? undefined
|