@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 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.18";
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: "#4F46E5",
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 = requireAccountAuthority(context.req.raw, state, "projects:read");
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 = requireAccountAuthority(context.req.raw, state, "projects:write");
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 = requireAccountAuthority(context.req.raw, state, "projects:write");
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 = requireAccountAuthority(context.req.raw, state, "projects:read");
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 = requireAccountAuthority(context.req.raw, state, "projects:write");
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 = requireAccountAuthority(context.req.raw, state, "projects:write");
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 identity = requireHumanSession(context.req.raw, state);
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 identity = requireHumanSession(context.req.raw, state);
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