@continuous-excellence/ze-great-dashboard-aws 0.30.6 → 0.31.1

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/README.md CHANGED
@@ -1,74 +1,56 @@
1
1
  # Ze Great Dashboard on AWS
2
2
 
3
- `@continuous-excellence/ze-great-dashboard-aws` packages a board for a private AWS Lambda or ECS
4
- deployment. It includes the Lambda runtime, deployment CLI, and CloudFormation templates; browser
5
- assets are published separately as `@continuous-excellence/ze-great-dashboard-client`.
6
-
7
- Deployment mode is persisted in `dashboard-bootstrap.json` and generated application parameters
8
- as `ComputeMode`. Existing files without that field mean `lambda`. Choose `--mode ecs` during
9
- bootstrap initialization; routine packaging and diagnostics then select the matching template.
10
- An explicit mode fails if it disagrees with persisted configuration, so changing mode requires
11
- regenerating the reviewed bootstrap and parameter artifacts.
12
-
13
- `dashboard-bootstrap.json` is checked-in desired state, not a capture of AWS. If an intentionally
14
- upgraded package changes a bootstrap template contract or revision, run `bootstrap upgrade
15
- --config dashboard-bootstrap.json`, review and commit only that metadata change, then have the
16
- consumer's approved deployment automation preview and execute CloudFormation UPDATE change sets.
17
- Package upgrades that do not change bootstrap template identity do not require a manifest update or
18
- bootstrap redeploy. Captures and generated parameter files are disposable deployment artifacts;
19
- the package owns template contents and the manifest records the intended package/template identity.
20
- No credentials belong in the manifest.
21
-
22
- Consumer ECS deployments use the long-lived service template and provide their own subnets and
23
- security groups.
24
-
25
- This deployment path is intended for teams that already operate AWS and have a protected gateway
26
- such as API Gateway or an ALB. It deliberately does not create a public endpoint, choose an
27
- authentication policy, or manage secret values.
28
-
29
- If you are evaluating the dashboard, start with the repository's
30
- [local setup](https://github.com/robertfmurdock/ze-great-dashboard#run-it-locally). You do not need
31
- AWS to try it.
32
-
33
- ## What you need
3
+ `@continuous-excellence/ze-great-dashboard-aws` validates and packages a board for a private AWS Lambda
4
+ or ECS deployment. It includes the server runtime, deployment CLI, and CloudFormation
5
+ templates. The matching immutable browser client is published separately as
6
+ `@continuous-excellence/ze-great-dashboard-client` and is selected automatically unless you provide
7
+ another exact asset URL.
8
+
9
+ This package is for teams that already operate AWS and can provide a protected entry path:
10
+
11
+ - Lambda requires a consumer-owned API Gateway, ALB, or equivalent gateway with private invocation
12
+ permission.
13
+ - ECS requires consumer-owned subnets, security groups, and protected load balancing or routing for
14
+ the long-lived Fargate service.
15
+
16
+ In both modes, a consumer-owned gateway or load balancer defines the public entry and authentication
17
+ boundary.
18
+
19
+ The templates create no public endpoint, choose no authentication policy, and manage no secret
20
+ values. To evaluate the dashboard without AWS, start with the
21
+ [local setup](https://github.com/robertfmurdock/ze-great-dashboard#try-it).
22
+
23
+ ## Prerequisites
34
24
 
35
25
  - Node.js 22 or newer, npm, the AWS CLI, and `jq`.
36
- - An AWS administrator for the one-time bootstrap.
37
- - A pre-existing central GitHub OIDC provider and a protected GitHub Environment if GitHub Actions
38
- will deploy the dashboard.
39
- - A consumer-owned gateway that can privately invoke Lambda and enforce your access policy.
40
-
41
- The included deployment works out of the box with public GitHub repositories and HTTP endpoints
42
- that do not require credentials. For private GitHub sources, `SecretReference` is the ARN of one
43
- consumer-owned Secrets Manager JSON map or Parameter Store `SecureString` (for example,
44
- `{"GITHUB_TOKEN":"github_pat_…"}`). The runtime resolves configured `token_env` names only at
45
- Lambda cold start and never exposes token values to the browser, API responses, logs,
46
- CloudFormation parameters, or Lambda environment. The two added AWS SDK clients provide
47
- IAM-authenticated `GetSecretValue` and decrypted `GetParameter` support in the bundled Lambda, at
48
- the cost of their bundled code and one cold-start request when private sources are configured.
49
-
50
- ## Deployment map
51
-
52
- 1. An administrator creates the artifact bucket and restricted deployment roles using the
53
- [AWS bootstrap guide](https://github.com/robertfmurdock/ze-great-dashboard/blob/main/docs/aws-bootstrap.md).
54
- 2. The application owner writes `board.yaml`, packages the Lambda, and deploys its private
55
- CloudFormation stack using the
56
- [deployment guide](https://github.com/robertfmurdock/ze-great-dashboard/blob/main/docs/aws-setup.md).
57
- 3. The consumer connects the returned `ServerFunctionArn` to its protected gateway.
58
- 4. GitHub Actions can repeat the package-and-deploy step after the administrator configures the two
59
- reviewed role ARNs.
60
-
61
- ## The normal application workflow
62
-
63
- Pin an exact package version in the repository that owns the deployment:
26
+ - An AWS administrator for one-time bootstrap.
27
+ - A pre-existing account-level GitHub OIDC provider and protected GitHub Environment when GitHub
28
+ Actions will deploy the dashboard.
29
+ - Consumer-owned networking, gateway or load balancing, authentication, and runtime health checks.
30
+
31
+ ## Choose and bootstrap a compute mode
32
+
33
+ Lambda is the compatibility default. Select ECS with `--mode ecs` when running `bootstrap init`.
34
+ The choice is stored in `dashboard-bootstrap.json` and generated application parameters as
35
+ `ComputeMode`; older files without the field mean Lambda.
36
+
37
+ An explicit mode that conflicts with persisted configuration fails. Changing between Lambda and ECS
38
+ requires regenerated, administrator-reviewed bootstrap and application parameters—it is not a
39
+ routine package option.
40
+
41
+ Follow the [AWS bootstrap guide](https://github.com/robertfmurdock/ze-great-dashboard/blob/main/docs/aws-bootstrap.md),
42
+ then the [Lambda deployment guide](https://github.com/robertfmurdock/ze-great-dashboard/blob/main/docs/aws-setup.md).
43
+
44
+ ## Package a deployment
45
+
46
+ Pin the reviewed package version in the repository that owns the deployment:
64
47
 
65
48
  ```sh
66
49
  npm install --save-exact @continuous-excellence/ze-great-dashboard-aws
67
50
  ```
68
51
 
69
- Append `@version` when installing a previously reviewed release rather than the current one.
70
-
71
- After bootstrap is complete, the recurring workflow is only:
52
+ Append `@version` when installing a previously reviewed release rather than the current one. After
53
+ bootstrap, generate and diagnose application parameters, then package the board:
72
54
 
73
55
  ```sh
74
56
  npm exec -- ze-great-dashboard-aws parameters \
@@ -85,29 +67,50 @@ npm exec -- ze-great-dashboard-aws package \
85
67
  --output aws-dashboard-release
86
68
  ```
87
69
 
88
- `package` writes the complete release-specific `parameters.json` and machine-readable
89
- `deployment.json` alongside the ZIP and template. The deployment guide covers executing the emitted
90
- S3 upload and CloudFormation commands, gateway handoff, and CI example.
91
- Changing `board.yaml` or upgrading the pinned package uses this same path.
70
+ For ECS, the persisted parameters select the ECS template and `package` also requires an immutable,
71
+ digest-pinned image through `--image`. The generated `deployment.json` contains structured upload
72
+ and CloudFormation command arguments for the approved deployment process to invoke.
92
73
 
93
- ## Package boundaries
74
+ `package` writes release-specific `parameters.json`, `deployment.json`, and `template.yml`, plus
75
+ `lambda.zip` for Lambda. Changing `board.yaml` or the pinned package version uses this same path.
76
+ Run the read-only `bootstrap check` before every deployment.
94
77
 
95
- | The package owns | You own |
96
- | --- | --- |
97
- | Board validation and Lambda packaging | Board content and source access |
98
- | Server runtime and CloudFormation templates | The immutable browser asset host and authentication |
99
- | Private application CloudFormation template | AWS account administration |
100
- | Restricted bootstrap templates | Secret values and the credential-map ARN |
101
- | Read-only preflight and consistency checks | Reviewing and executing AWS changes |
78
+ ## Private source credentials
102
79
 
103
- Bootstrap commands never execute mutating AWS operations. They produce plans, parameters, and
104
- commands for an administrator to review and run explicitly. `bootstrap check` is the named
105
- read-only live diagnostic used before routine deployments.
80
+ Public GitHub repositories and unauthenticated HTTP sources work without credentials. For private
81
+ sources, set `SecretReference` to the ARN of one consumer-owned Secrets Manager JSON value or
82
+ Parameter Store `SecureString`, for example:
83
+
84
+ ```json
85
+ {"GITHUB_TOKEN":"github_pat_…"}
86
+ ```
87
+
88
+ The board references `GITHUB_TOKEN` through `token_env`; the credential value never belongs in the
89
+ board, bootstrap manifest, CloudFormation parameters, runtime environment variables, logs, API
90
+ responses, or browser. The runtime reads only the exact secret or parameter authorized by the
91
+ application role, resolves configured `token_env` names only at Lambda cold start, and fails closed
92
+ when a key is absent.
93
+
94
+ ## Ownership boundaries
95
+
96
+ | Package responsibility | Consumer responsibility |
97
+ | --- | --- |
98
+ | Board validation and release packaging | Board content and source permissions |
99
+ | Server runtime and application templates | AWS account and network administration |
100
+ | Restricted bootstrap templates | Reviewing and executing AWS changes |
101
+ | Default immutable client asset selection | Approval of alternate asset hosts |
102
+ | Read-only preflight, doctor, and consistency checks | Gateway, authentication, and health checks |
103
+ | Exact secret-resource integration | Secret values, rotation, and credential-map ARN |
104
+
105
+ Bootstrap commands do not perform mutating AWS operations. They emit plans, parameters, captures,
106
+ and command arguments for an administrator to review and invoke. `bootstrap check` is the read-only
107
+ live diagnostic used before routine deployments; when it fails, use the
108
+ [bootstrap upgrade runbook](https://github.com/robertfmurdock/ze-great-dashboard/blob/main/docs/aws-bootstrap-upgrade.md).
106
109
 
107
110
  ## References
108
111
 
109
112
  - [Board configuration](https://github.com/robertfmurdock/ze-great-dashboard/blob/main/docs/board-configuration.md)
110
113
  - [Administrator bootstrap](https://github.com/robertfmurdock/ze-great-dashboard/blob/main/docs/aws-bootstrap.md)
111
- - [Application deployment](https://github.com/robertfmurdock/ze-great-dashboard/blob/main/docs/aws-setup.md)
114
+ - [Lambda deployment](https://github.com/robertfmurdock/ze-great-dashboard/blob/main/docs/aws-setup.md)
112
115
  - [GitHub Actions deployment](https://github.com/robertfmurdock/ze-great-dashboard/blob/main/docs/aws-github-actions.md)
113
116
  - [Issue tracker](https://github.com/robertfmurdock/ze-great-dashboard/issues)
package/dist/cli.js CHANGED
@@ -320,14 +320,41 @@ var boardSchema = z2.object({
320
320
  params: { constraint: "Important panels require one board attention treatment." }
321
321
  });
322
322
  });
323
+ var authorizationSchema = z2.discriminatedUnion("mode", [
324
+ z2.strictObject({ mode: z2.literal("authenticated") }),
325
+ z2.strictObject({ mode: z2.literal("subjects"), subjects: z2.array(z2.string().min(1)).min(1) }),
326
+ z2.strictObject({
327
+ mode: z2.literal("claim"),
328
+ /** Exact JWT payload key; namespaced provider claims are keys, never path expressions. */
329
+ claim: z2.string().min(1),
330
+ values: z2.array(z2.string().min(1)).min(1),
331
+ match: z2.enum(["any", "all"])
332
+ })
333
+ ]);
334
+ var legacySubjectAllowSchema = z2.strictObject({ subjects: z2.array(z2.string().min(1)).min(1) });
323
335
  var authSchema = z2.strictObject({
324
336
  issuer: z2.url(),
325
337
  /** Browser SPA client id. It is public, unlike every source credential. */
326
338
  client_id: z2.string().min(1),
327
339
  /** The separately registered API resource accepted by the dashboard proxy. */
328
340
  audience: z2.string().min(1),
329
- /** A deliberately portable, explicit identity boundary for the first direct-OIDC release. */
330
- allow: z2.strictObject({ subjects: z2.array(z2.string().min(1)).min(1) })
341
+ /** Canonical authorization policy. */
342
+ authorization: authorizationSchema.optional(),
343
+ /** Legacy subject policy retained only for a configuration migration. */
344
+ allow: legacySubjectAllowSchema.optional()
345
+ }).superRefine((auth, ctx) => {
346
+ if (!auth.authorization && !auth.allow)
347
+ ctx.addIssue({
348
+ code: "custom",
349
+ path: ["authorization"],
350
+ message: "configure authorization, or the legacy allow.subjects migration policy"
351
+ });
352
+ if (auth.authorization && auth.allow)
353
+ ctx.addIssue({
354
+ code: "custom",
355
+ path: ["authorization"],
356
+ message: "configure authorization or legacy allow.subjects, not both"
357
+ });
331
358
  });
332
359
  var securityPolicies = ["warn", "required", "unsecured"];
333
360
  var securityPolicySchema = z2.enum(securityPolicies);
@@ -537,43 +564,58 @@ var httpValueSchema = z4.object({
537
564
  value: z4.union([z4.string(), z4.number(), z4.boolean()])
538
565
  });
539
566
 
540
- // packages/server/src/adapters/azure-devops.ts
567
+ // packages/shared/src/security-details.ts
541
568
  import { z as z5 } from "zod";
542
- var pipelinePanelSchema = z5.object({
543
- id: z5.string().min(1),
544
- type: z5.literal("pipeline-status"),
545
- source: z5.string().min(1),
546
- pipeline: z5.number().int().positive()
569
+ var securityDetailsResponseSchema = z5.strictObject({
570
+ oidc: z5.literal(true),
571
+ policy: z5.discriminatedUnion("mode", [
572
+ z5.strictObject({ mode: z5.literal("authenticated") }),
573
+ z5.strictObject({ mode: z5.literal("subjects") }),
574
+ z5.strictObject({
575
+ mode: z5.literal("claim"),
576
+ claim: z5.string().min(1),
577
+ match: z5.enum(["any", "all"])
578
+ })
579
+ ])
580
+ });
581
+
582
+ // packages/server/src/adapters/azure-devops.ts
583
+ import { z as z6 } from "zod";
584
+ var pipelinePanelSchema = z6.object({
585
+ id: z6.string().min(1),
586
+ type: z6.literal("pipeline-status"),
587
+ source: z6.string().min(1),
588
+ pipeline: z6.number().int().positive()
547
589
  });
548
- var buildSchema = z5.object({
549
- id: z5.number().int().positive(),
550
- buildNumber: z5.string().min(1),
551
- status: z5.string().nullable().optional(),
552
- result: z5.string().nullable().optional(),
553
- sourceBranch: z5.string().min(1).nullable().optional(),
554
- startTime: z5.string().nullable().optional(),
555
- finishTime: z5.string().nullable().optional(),
556
- lastChangedDate: z5.string().nullable().optional()
590
+ var buildSchema = z6.object({
591
+ id: z6.number().int().positive(),
592
+ buildNumber: z6.string().min(1),
593
+ status: z6.string().nullable().optional(),
594
+ result: z6.string().nullable().optional(),
595
+ sourceBranch: z6.string().min(1).nullable().optional(),
596
+ startTime: z6.string().nullable().optional(),
597
+ finishTime: z6.string().nullable().optional(),
598
+ lastChangedDate: z6.string().nullable().optional()
557
599
  });
558
- var buildsSchema = z5.object({ value: z5.array(buildSchema) });
559
- var timelineSchema = z5.object({
560
- records: z5.array(
561
- z5.object({
562
- id: z5.string().min(1),
563
- parentId: z5.string().min(1).nullable().optional(),
564
- type: z5.string().min(1),
565
- name: z5.string().min(1),
566
- state: z5.string().nullable().optional()
600
+ var buildsSchema = z6.object({ value: z6.array(buildSchema) });
601
+ var timelineSchema = z6.object({
602
+ records: z6.array(
603
+ z6.object({
604
+ id: z6.string().min(1),
605
+ parentId: z6.string().min(1).nullable().optional(),
606
+ type: z6.string().min(1),
607
+ name: z6.string().min(1),
608
+ state: z6.string().nullable().optional()
567
609
  })
568
610
  )
569
611
  });
570
612
  function permittedAzureDevOpsCalls(panel, source) {
571
- const { panel: parsedPanel, source: parsedSource } = z5.object({ panel: pipelinePanelSchema, source: azureDevOpsSourceSchema }).parse({ panel, source });
613
+ const { panel: parsedPanel, source: parsedSource } = z6.object({ panel: pipelinePanelSchema, source: azureDevOpsSourceSchema }).parse({ panel, source });
572
614
  return [buildsCall(parsedPanel.pipeline, parsedSource)];
573
615
  }
574
- var delegatedTokenFileSchema = z5.object({
575
- accessToken: z5.string().min(1),
576
- expiresAt: z5.string().min(1)
616
+ var delegatedTokenFileSchema = z6.object({
617
+ accessToken: z6.string().min(1),
618
+ expiresAt: z6.string().min(1)
577
619
  });
578
620
  function buildsCall(definitionId, source) {
579
621
  const url = apiUrl(source, "build/builds");
@@ -595,59 +637,59 @@ function adoBranch(branch) {
595
637
  }
596
638
 
597
639
  // packages/server/src/adapters/github-actions.ts
598
- import { z as z6 } from "zod";
599
- var pipelinePanelSchema2 = z6.object({
600
- id: z6.string().min(1),
601
- type: z6.literal("pipeline-status"),
602
- source: z6.string().min(1),
640
+ import { z as z7 } from "zod";
641
+ var pipelinePanelSchema2 = z7.object({
642
+ id: z7.string().min(1),
643
+ type: z7.literal("pipeline-status"),
644
+ source: z7.string().min(1),
603
645
  /** GitHub accepts a workflow filename or numeric workflow id in this URL segment. */
604
- pipeline: z6.string().min(1)
646
+ pipeline: z7.string().min(1)
605
647
  });
606
- var pullRequestHealthPanelSchema = z6.object({
607
- id: z6.string().min(1),
608
- type: z6.literal("pull-request-health"),
609
- source: z6.string().min(1),
610
- base_branch: z6.string().min(1),
611
- update_workflows: z6.array(
612
- z6.object({
613
- workflow: z6.string().min(1),
614
- branch_prefixes: z6.array(z6.string().min(1)).min(1)
648
+ var pullRequestHealthPanelSchema = z7.object({
649
+ id: z7.string().min(1),
650
+ type: z7.literal("pull-request-health"),
651
+ source: z7.string().min(1),
652
+ base_branch: z7.string().min(1),
653
+ update_workflows: z7.array(
654
+ z7.object({
655
+ workflow: z7.string().min(1),
656
+ branch_prefixes: z7.array(z7.string().min(1)).min(1)
615
657
  })
616
658
  ).min(1),
617
- build_workflow: z6.string().min(1)
659
+ build_workflow: z7.string().min(1)
618
660
  });
619
- var runSchema = z6.object({
620
- id: z6.number().int().positive().optional(),
621
- status: z6.string(),
622
- conclusion: z6.string().nullable(),
623
- name: z6.string().min(1),
624
- html_url: z6.url(),
661
+ var runSchema = z7.object({
662
+ id: z7.number().int().positive().optional(),
663
+ status: z7.string(),
664
+ conclusion: z7.string().nullable(),
665
+ name: z7.string().min(1),
666
+ html_url: z7.url(),
625
667
  /** The branch that produced this run; absent for some event types. */
626
- head_branch: z6.string().min(1).nullable().optional(),
668
+ head_branch: z7.string().min(1).nullable().optional(),
627
669
  // Timestamp quality should only remove timing advice, never make an otherwise useful run unreadable.
628
- run_started_at: z6.string().nullable().optional(),
629
- updated_at: z6.string().optional()
670
+ run_started_at: z7.string().nullable().optional(),
671
+ updated_at: z7.string().optional()
630
672
  });
631
- var runsSchema = z6.object({ workflow_runs: z6.array(runSchema) });
632
- var jobsSchema = z6.object({
633
- jobs: z6.array(
634
- z6.object({
635
- name: z6.string().min(1),
636
- status: z6.string(),
637
- steps: z6.array(z6.object({ name: z6.string().min(1), status: z6.string() })).optional()
673
+ var runsSchema = z7.object({ workflow_runs: z7.array(runSchema) });
674
+ var jobsSchema = z7.object({
675
+ jobs: z7.array(
676
+ z7.object({
677
+ name: z7.string().min(1),
678
+ status: z7.string(),
679
+ steps: z7.array(z7.object({ name: z7.string().min(1), status: z7.string() })).optional()
638
680
  })
639
681
  )
640
682
  });
641
- var pullRequestsSchema = z6.array(
642
- z6.object({
643
- number: z6.number().int().positive(),
644
- html_url: z6.url(),
645
- head: z6.object({ ref: z6.string().min(1) }),
646
- base: z6.object({ ref: z6.string().min(1) })
683
+ var pullRequestsSchema = z7.array(
684
+ z7.object({
685
+ number: z7.number().int().positive(),
686
+ html_url: z7.url(),
687
+ head: z7.object({ ref: z7.string().min(1) }),
688
+ base: z7.object({ ref: z7.string().min(1) })
647
689
  })
648
690
  );
649
691
  function permittedGithubActionsCalls(panel, source, _credentials) {
650
- const { panel: parsedPanel, source: parsedSource } = z6.object({ panel: pipelinePanelSchema2, source: githubActionsSourceSchema }).parse({ panel, source });
692
+ const { panel: parsedPanel, source: parsedSource } = z7.object({ panel: pipelinePanelSchema2, source: githubActionsSourceSchema }).parse({ panel, source });
651
693
  const url = new URL(
652
694
  `https://api.github.com/repos/${parsedSource.repo}/actions/workflows/${encodeURIComponent(parsedPanel.pipeline)}/runs`
653
695
  );
@@ -656,7 +698,7 @@ function permittedGithubActionsCalls(panel, source, _credentials) {
656
698
  return [{ url: url.toString(), headers: new Headers() }];
657
699
  }
658
700
  function pullRequestHealthCapabilities(panel) {
659
- const { panel: parsed } = z6.object({ panel: pullRequestHealthPanelSchema }).parse({ panel });
701
+ const { panel: parsed } = z7.object({ panel: pullRequestHealthPanelSchema }).parse({ panel });
660
702
  return {
661
703
  panel: parsed,
662
704
  configuredUpdateWorkflow(workflow) {
@@ -671,23 +713,23 @@ function pullRequestHealthCapabilities(panel) {
671
713
  }
672
714
 
673
715
  // packages/server/src/adapters/gitlab-ci.ts
674
- import { z as z7 } from "zod";
675
- var pipelinePanelSchema3 = z7.object({
676
- id: z7.string().min(1),
677
- type: z7.literal("pipeline-status"),
678
- source: z7.string().min(1)
716
+ import { z as z8 } from "zod";
717
+ var pipelinePanelSchema3 = z8.object({
718
+ id: z8.string().min(1),
719
+ type: z8.literal("pipeline-status"),
720
+ source: z8.string().min(1)
679
721
  });
680
- var pipelineSchema = z7.object({
681
- id: z7.number().int().positive(),
682
- name: z7.string().min(1).nullable().optional(),
683
- status: z7.string(),
684
- ref: z7.string().min(1).nullable().optional(),
685
- created_at: z7.string().nullable().optional(),
686
- updated_at: z7.string().nullable().optional(),
687
- web_url: z7.url()
722
+ var pipelineSchema = z8.object({
723
+ id: z8.number().int().positive(),
724
+ name: z8.string().min(1).nullable().optional(),
725
+ status: z8.string(),
726
+ ref: z8.string().min(1).nullable().optional(),
727
+ created_at: z8.string().nullable().optional(),
728
+ updated_at: z8.string().nullable().optional(),
729
+ web_url: z8.url()
688
730
  });
689
731
  function permittedGitlabCiCalls(panel, source) {
690
- const { source: parsedSource } = z7.object({ panel: pipelinePanelSchema3, source: gitlabCiSourceSchema }).parse({ panel, source });
732
+ const { source: parsedSource } = z8.object({ panel: pipelinePanelSchema3, source: gitlabCiSourceSchema }).parse({ panel, source });
691
733
  return [pipelinesCall(parsedSource)];
692
734
  }
693
735
  function pipelinesCall(source) {
@@ -699,10 +741,10 @@ function pipelinesCall(source) {
699
741
  }
700
742
 
701
743
  // packages/server/src/adapters/http-value.ts
702
- import { z as z8 } from "zod";
744
+ import { z as z9 } from "zod";
703
745
  function permittedHttpValueCalls(panel) {
704
746
  const grouped = httpValueGroupedPanelSchema.safeParse(panel);
705
- const urls = grouped.success ? grouped.data.facts.map((fact) => fact.url) : [z8.object({ panel: httpValueScalarPanelSchema }).parse({ panel }).panel.url];
747
+ const urls = grouped.success ? grouped.data.facts.map((fact) => fact.url) : [z9.object({ panel: httpValueScalarPanelSchema }).parse({ panel }).panel.url];
706
748
  return urls.map(permittedCall);
707
749
  }
708
750
  function permittedCall(value2) {
package/dist/index.js CHANGED
@@ -1001,14 +1001,41 @@ var boardSchema = z2.object({
1001
1001
  params: { constraint: "Important panels require one board attention treatment." }
1002
1002
  });
1003
1003
  });
1004
+ var authorizationSchema = z2.discriminatedUnion("mode", [
1005
+ z2.strictObject({ mode: z2.literal("authenticated") }),
1006
+ z2.strictObject({ mode: z2.literal("subjects"), subjects: z2.array(z2.string().min(1)).min(1) }),
1007
+ z2.strictObject({
1008
+ mode: z2.literal("claim"),
1009
+ /** Exact JWT payload key; namespaced provider claims are keys, never path expressions. */
1010
+ claim: z2.string().min(1),
1011
+ values: z2.array(z2.string().min(1)).min(1),
1012
+ match: z2.enum(["any", "all"])
1013
+ })
1014
+ ]);
1015
+ var legacySubjectAllowSchema = z2.strictObject({ subjects: z2.array(z2.string().min(1)).min(1) });
1004
1016
  var authSchema = z2.strictObject({
1005
1017
  issuer: z2.url(),
1006
1018
  /** Browser SPA client id. It is public, unlike every source credential. */
1007
1019
  client_id: z2.string().min(1),
1008
1020
  /** The separately registered API resource accepted by the dashboard proxy. */
1009
1021
  audience: z2.string().min(1),
1010
- /** A deliberately portable, explicit identity boundary for the first direct-OIDC release. */
1011
- allow: z2.strictObject({ subjects: z2.array(z2.string().min(1)).min(1) })
1022
+ /** Canonical authorization policy. */
1023
+ authorization: authorizationSchema.optional(),
1024
+ /** Legacy subject policy retained only for a configuration migration. */
1025
+ allow: legacySubjectAllowSchema.optional()
1026
+ }).superRefine((auth, ctx) => {
1027
+ if (!auth.authorization && !auth.allow)
1028
+ ctx.addIssue({
1029
+ code: "custom",
1030
+ path: ["authorization"],
1031
+ message: "configure authorization, or the legacy allow.subjects migration policy"
1032
+ });
1033
+ if (auth.authorization && auth.allow)
1034
+ ctx.addIssue({
1035
+ code: "custom",
1036
+ path: ["authorization"],
1037
+ message: "configure authorization or legacy allow.subjects, not both"
1038
+ });
1012
1039
  });
1013
1040
  var securityPolicies = ["warn", "required", "unsecured"];
1014
1041
  var securityPolicySchema = z2.enum(securityPolicies);
@@ -1218,43 +1245,58 @@ var httpValueSchema = z4.object({
1218
1245
  value: z4.union([z4.string(), z4.number(), z4.boolean()])
1219
1246
  });
1220
1247
 
1221
- // packages/server/src/adapters/azure-devops.ts
1248
+ // packages/shared/src/security-details.ts
1222
1249
  import { z as z5 } from "zod";
1223
- var pipelinePanelSchema = z5.object({
1224
- id: z5.string().min(1),
1225
- type: z5.literal("pipeline-status"),
1226
- source: z5.string().min(1),
1227
- pipeline: z5.number().int().positive()
1250
+ var securityDetailsResponseSchema = z5.strictObject({
1251
+ oidc: z5.literal(true),
1252
+ policy: z5.discriminatedUnion("mode", [
1253
+ z5.strictObject({ mode: z5.literal("authenticated") }),
1254
+ z5.strictObject({ mode: z5.literal("subjects") }),
1255
+ z5.strictObject({
1256
+ mode: z5.literal("claim"),
1257
+ claim: z5.string().min(1),
1258
+ match: z5.enum(["any", "all"])
1259
+ })
1260
+ ])
1228
1261
  });
1229
- var buildSchema = z5.object({
1230
- id: z5.number().int().positive(),
1231
- buildNumber: z5.string().min(1),
1232
- status: z5.string().nullable().optional(),
1233
- result: z5.string().nullable().optional(),
1234
- sourceBranch: z5.string().min(1).nullable().optional(),
1235
- startTime: z5.string().nullable().optional(),
1236
- finishTime: z5.string().nullable().optional(),
1237
- lastChangedDate: z5.string().nullable().optional()
1262
+
1263
+ // packages/server/src/adapters/azure-devops.ts
1264
+ import { z as z6 } from "zod";
1265
+ var pipelinePanelSchema = z6.object({
1266
+ id: z6.string().min(1),
1267
+ type: z6.literal("pipeline-status"),
1268
+ source: z6.string().min(1),
1269
+ pipeline: z6.number().int().positive()
1270
+ });
1271
+ var buildSchema = z6.object({
1272
+ id: z6.number().int().positive(),
1273
+ buildNumber: z6.string().min(1),
1274
+ status: z6.string().nullable().optional(),
1275
+ result: z6.string().nullable().optional(),
1276
+ sourceBranch: z6.string().min(1).nullable().optional(),
1277
+ startTime: z6.string().nullable().optional(),
1278
+ finishTime: z6.string().nullable().optional(),
1279
+ lastChangedDate: z6.string().nullable().optional()
1238
1280
  });
1239
- var buildsSchema = z5.object({ value: z5.array(buildSchema) });
1240
- var timelineSchema = z5.object({
1241
- records: z5.array(
1242
- z5.object({
1243
- id: z5.string().min(1),
1244
- parentId: z5.string().min(1).nullable().optional(),
1245
- type: z5.string().min(1),
1246
- name: z5.string().min(1),
1247
- state: z5.string().nullable().optional()
1281
+ var buildsSchema = z6.object({ value: z6.array(buildSchema) });
1282
+ var timelineSchema = z6.object({
1283
+ records: z6.array(
1284
+ z6.object({
1285
+ id: z6.string().min(1),
1286
+ parentId: z6.string().min(1).nullable().optional(),
1287
+ type: z6.string().min(1),
1288
+ name: z6.string().min(1),
1289
+ state: z6.string().nullable().optional()
1248
1290
  })
1249
1291
  )
1250
1292
  });
1251
1293
  function permittedAzureDevOpsCalls(panel, source) {
1252
- const { panel: parsedPanel, source: parsedSource } = z5.object({ panel: pipelinePanelSchema, source: azureDevOpsSourceSchema }).parse({ panel, source });
1294
+ const { panel: parsedPanel, source: parsedSource } = z6.object({ panel: pipelinePanelSchema, source: azureDevOpsSourceSchema }).parse({ panel, source });
1253
1295
  return [buildsCall(parsedPanel.pipeline, parsedSource)];
1254
1296
  }
1255
- var delegatedTokenFileSchema = z5.object({
1256
- accessToken: z5.string().min(1),
1257
- expiresAt: z5.string().min(1)
1297
+ var delegatedTokenFileSchema = z6.object({
1298
+ accessToken: z6.string().min(1),
1299
+ expiresAt: z6.string().min(1)
1258
1300
  });
1259
1301
  function buildsCall(definitionId, source) {
1260
1302
  const url = apiUrl(source, "build/builds");
@@ -1276,59 +1318,59 @@ function adoBranch(branch) {
1276
1318
  }
1277
1319
 
1278
1320
  // packages/server/src/adapters/github-actions.ts
1279
- import { z as z6 } from "zod";
1280
- var pipelinePanelSchema2 = z6.object({
1281
- id: z6.string().min(1),
1282
- type: z6.literal("pipeline-status"),
1283
- source: z6.string().min(1),
1321
+ import { z as z7 } from "zod";
1322
+ var pipelinePanelSchema2 = z7.object({
1323
+ id: z7.string().min(1),
1324
+ type: z7.literal("pipeline-status"),
1325
+ source: z7.string().min(1),
1284
1326
  /** GitHub accepts a workflow filename or numeric workflow id in this URL segment. */
1285
- pipeline: z6.string().min(1)
1327
+ pipeline: z7.string().min(1)
1286
1328
  });
1287
- var pullRequestHealthPanelSchema = z6.object({
1288
- id: z6.string().min(1),
1289
- type: z6.literal("pull-request-health"),
1290
- source: z6.string().min(1),
1291
- base_branch: z6.string().min(1),
1292
- update_workflows: z6.array(
1293
- z6.object({
1294
- workflow: z6.string().min(1),
1295
- branch_prefixes: z6.array(z6.string().min(1)).min(1)
1329
+ var pullRequestHealthPanelSchema = z7.object({
1330
+ id: z7.string().min(1),
1331
+ type: z7.literal("pull-request-health"),
1332
+ source: z7.string().min(1),
1333
+ base_branch: z7.string().min(1),
1334
+ update_workflows: z7.array(
1335
+ z7.object({
1336
+ workflow: z7.string().min(1),
1337
+ branch_prefixes: z7.array(z7.string().min(1)).min(1)
1296
1338
  })
1297
1339
  ).min(1),
1298
- build_workflow: z6.string().min(1)
1340
+ build_workflow: z7.string().min(1)
1299
1341
  });
1300
- var runSchema = z6.object({
1301
- id: z6.number().int().positive().optional(),
1302
- status: z6.string(),
1303
- conclusion: z6.string().nullable(),
1304
- name: z6.string().min(1),
1305
- html_url: z6.url(),
1342
+ var runSchema = z7.object({
1343
+ id: z7.number().int().positive().optional(),
1344
+ status: z7.string(),
1345
+ conclusion: z7.string().nullable(),
1346
+ name: z7.string().min(1),
1347
+ html_url: z7.url(),
1306
1348
  /** The branch that produced this run; absent for some event types. */
1307
- head_branch: z6.string().min(1).nullable().optional(),
1349
+ head_branch: z7.string().min(1).nullable().optional(),
1308
1350
  // Timestamp quality should only remove timing advice, never make an otherwise useful run unreadable.
1309
- run_started_at: z6.string().nullable().optional(),
1310
- updated_at: z6.string().optional()
1351
+ run_started_at: z7.string().nullable().optional(),
1352
+ updated_at: z7.string().optional()
1311
1353
  });
1312
- var runsSchema = z6.object({ workflow_runs: z6.array(runSchema) });
1313
- var jobsSchema = z6.object({
1314
- jobs: z6.array(
1315
- z6.object({
1316
- name: z6.string().min(1),
1317
- status: z6.string(),
1318
- steps: z6.array(z6.object({ name: z6.string().min(1), status: z6.string() })).optional()
1354
+ var runsSchema = z7.object({ workflow_runs: z7.array(runSchema) });
1355
+ var jobsSchema = z7.object({
1356
+ jobs: z7.array(
1357
+ z7.object({
1358
+ name: z7.string().min(1),
1359
+ status: z7.string(),
1360
+ steps: z7.array(z7.object({ name: z7.string().min(1), status: z7.string() })).optional()
1319
1361
  })
1320
1362
  )
1321
1363
  });
1322
- var pullRequestsSchema = z6.array(
1323
- z6.object({
1324
- number: z6.number().int().positive(),
1325
- html_url: z6.url(),
1326
- head: z6.object({ ref: z6.string().min(1) }),
1327
- base: z6.object({ ref: z6.string().min(1) })
1364
+ var pullRequestsSchema = z7.array(
1365
+ z7.object({
1366
+ number: z7.number().int().positive(),
1367
+ html_url: z7.url(),
1368
+ head: z7.object({ ref: z7.string().min(1) }),
1369
+ base: z7.object({ ref: z7.string().min(1) })
1328
1370
  })
1329
1371
  );
1330
1372
  function permittedGithubActionsCalls(panel, source, _credentials) {
1331
- const { panel: parsedPanel, source: parsedSource } = z6.object({ panel: pipelinePanelSchema2, source: githubActionsSourceSchema }).parse({ panel, source });
1373
+ const { panel: parsedPanel, source: parsedSource } = z7.object({ panel: pipelinePanelSchema2, source: githubActionsSourceSchema }).parse({ panel, source });
1332
1374
  const url = new URL(
1333
1375
  `https://api.github.com/repos/${parsedSource.repo}/actions/workflows/${encodeURIComponent(parsedPanel.pipeline)}/runs`
1334
1376
  );
@@ -1337,7 +1379,7 @@ function permittedGithubActionsCalls(panel, source, _credentials) {
1337
1379
  return [{ url: url.toString(), headers: new Headers() }];
1338
1380
  }
1339
1381
  function pullRequestHealthCapabilities(panel) {
1340
- const { panel: parsed } = z6.object({ panel: pullRequestHealthPanelSchema }).parse({ panel });
1382
+ const { panel: parsed } = z7.object({ panel: pullRequestHealthPanelSchema }).parse({ panel });
1341
1383
  return {
1342
1384
  panel: parsed,
1343
1385
  configuredUpdateWorkflow(workflow) {
@@ -1352,23 +1394,23 @@ function pullRequestHealthCapabilities(panel) {
1352
1394
  }
1353
1395
 
1354
1396
  // packages/server/src/adapters/gitlab-ci.ts
1355
- import { z as z7 } from "zod";
1356
- var pipelinePanelSchema3 = z7.object({
1357
- id: z7.string().min(1),
1358
- type: z7.literal("pipeline-status"),
1359
- source: z7.string().min(1)
1397
+ import { z as z8 } from "zod";
1398
+ var pipelinePanelSchema3 = z8.object({
1399
+ id: z8.string().min(1),
1400
+ type: z8.literal("pipeline-status"),
1401
+ source: z8.string().min(1)
1360
1402
  });
1361
- var pipelineSchema = z7.object({
1362
- id: z7.number().int().positive(),
1363
- name: z7.string().min(1).nullable().optional(),
1364
- status: z7.string(),
1365
- ref: z7.string().min(1).nullable().optional(),
1366
- created_at: z7.string().nullable().optional(),
1367
- updated_at: z7.string().nullable().optional(),
1368
- web_url: z7.url()
1403
+ var pipelineSchema = z8.object({
1404
+ id: z8.number().int().positive(),
1405
+ name: z8.string().min(1).nullable().optional(),
1406
+ status: z8.string(),
1407
+ ref: z8.string().min(1).nullable().optional(),
1408
+ created_at: z8.string().nullable().optional(),
1409
+ updated_at: z8.string().nullable().optional(),
1410
+ web_url: z8.url()
1369
1411
  });
1370
1412
  function permittedGitlabCiCalls(panel, source) {
1371
- const { source: parsedSource } = z7.object({ panel: pipelinePanelSchema3, source: gitlabCiSourceSchema }).parse({ panel, source });
1413
+ const { source: parsedSource } = z8.object({ panel: pipelinePanelSchema3, source: gitlabCiSourceSchema }).parse({ panel, source });
1372
1414
  return [pipelinesCall(parsedSource)];
1373
1415
  }
1374
1416
  function pipelinesCall(source) {
@@ -1380,10 +1422,10 @@ function pipelinesCall(source) {
1380
1422
  }
1381
1423
 
1382
1424
  // packages/server/src/adapters/http-value.ts
1383
- import { z as z8 } from "zod";
1425
+ import { z as z9 } from "zod";
1384
1426
  function permittedHttpValueCalls(panel) {
1385
1427
  const grouped = httpValueGroupedPanelSchema.safeParse(panel);
1386
- const urls = grouped.success ? grouped.data.facts.map((fact) => fact.url) : [z8.object({ panel: httpValueScalarPanelSchema }).parse({ panel }).panel.url];
1428
+ const urls = grouped.success ? grouped.data.facts.map((fact) => fact.url) : [z9.object({ panel: httpValueScalarPanelSchema }).parse({ panel }).panel.url];
1387
1429
  return urls.map(permittedCall);
1388
1430
  }
1389
1431
  function permittedCall(value2) {
package/dist/lambda.mjs CHANGED
@@ -69356,15 +69356,47 @@ var boardSchema = external_exports.object({
69356
69356
  params: { constraint: "Important panels require one board attention treatment." }
69357
69357
  });
69358
69358
  });
69359
+ var authorizationSchema = external_exports.discriminatedUnion("mode", [
69360
+ external_exports.strictObject({ mode: external_exports.literal("authenticated") }),
69361
+ external_exports.strictObject({ mode: external_exports.literal("subjects"), subjects: external_exports.array(external_exports.string().min(1)).min(1) }),
69362
+ external_exports.strictObject({
69363
+ mode: external_exports.literal("claim"),
69364
+ /** Exact JWT payload key; namespaced provider claims are keys, never path expressions. */
69365
+ claim: external_exports.string().min(1),
69366
+ values: external_exports.array(external_exports.string().min(1)).min(1),
69367
+ match: external_exports.enum(["any", "all"])
69368
+ })
69369
+ ]);
69370
+ var legacySubjectAllowSchema = external_exports.strictObject({ subjects: external_exports.array(external_exports.string().min(1)).min(1) });
69359
69371
  var authSchema = external_exports.strictObject({
69360
69372
  issuer: external_exports.url(),
69361
69373
  /** Browser SPA client id. It is public, unlike every source credential. */
69362
69374
  client_id: external_exports.string().min(1),
69363
69375
  /** The separately registered API resource accepted by the dashboard proxy. */
69364
69376
  audience: external_exports.string().min(1),
69365
- /** A deliberately portable, explicit identity boundary for the first direct-OIDC release. */
69366
- allow: external_exports.strictObject({ subjects: external_exports.array(external_exports.string().min(1)).min(1) })
69377
+ /** Canonical authorization policy. */
69378
+ authorization: authorizationSchema.optional(),
69379
+ /** Legacy subject policy retained only for a configuration migration. */
69380
+ allow: legacySubjectAllowSchema.optional()
69381
+ }).superRefine((auth, ctx) => {
69382
+ if (!auth.authorization && !auth.allow)
69383
+ ctx.addIssue({
69384
+ code: "custom",
69385
+ path: ["authorization"],
69386
+ message: "configure authorization, or the legacy allow.subjects migration policy"
69387
+ });
69388
+ if (auth.authorization && auth.allow)
69389
+ ctx.addIssue({
69390
+ code: "custom",
69391
+ path: ["authorization"],
69392
+ message: "configure authorization or legacy allow.subjects, not both"
69393
+ });
69367
69394
  });
69395
+ function authorizationForAuth(auth) {
69396
+ if (auth.authorization) return auth.authorization;
69397
+ if (auth.allow) return { mode: "subjects", subjects: auth.allow.subjects };
69398
+ throw new Error("OIDC authorization policy is missing.");
69399
+ }
69368
69400
  var securityPolicies = ["warn", "required", "unsecured"];
69369
69401
  var securityPolicySchema = external_exports.enum(securityPolicies);
69370
69402
  var boardConfigSchema = external_exports.object({
@@ -69647,6 +69679,20 @@ function nearestLegalRectangle(desired, accepted) {
69647
69679
  return { x: candidate.x, y: candidate.y, w: candidate.w, h: candidate.h };
69648
69680
  }
69649
69681
 
69682
+ // packages/shared/src/security-details.ts
69683
+ var securityDetailsResponseSchema = external_exports.strictObject({
69684
+ oidc: external_exports.literal(true),
69685
+ policy: external_exports.discriminatedUnion("mode", [
69686
+ external_exports.strictObject({ mode: external_exports.literal("authenticated") }),
69687
+ external_exports.strictObject({ mode: external_exports.literal("subjects") }),
69688
+ external_exports.strictObject({
69689
+ mode: external_exports.literal("claim"),
69690
+ claim: external_exports.string().min(1),
69691
+ match: external_exports.enum(["any", "all"])
69692
+ })
69693
+ ])
69694
+ });
69695
+
69650
69696
  // packages/server/src/adapters/azure-devops.ts
69651
69697
  import { readFile } from "node:fs/promises";
69652
69698
 
@@ -73865,14 +73911,22 @@ async function createAccessTokenVerifier(auth, fetcher = globalThis.fetch) {
73865
73911
  const { payload: payload2 } = await jwtVerify(token, keys, { issuer, audience: auth.audience });
73866
73912
  if (typeof payload2.sub !== "string" || !payload2.sub)
73867
73913
  throw new Error("Access token has no subject.");
73868
- if (!auth.allow.subjects.includes(payload2.sub)) {
73914
+ if (!permitsIdentity(authorizationForAuth(auth), payload2.sub, payload2)) {
73869
73915
  const error63 = new Error("Authenticated subject is not allowed to view this dashboard.");
73870
- error63.name = "UnauthorizedSubjectError";
73916
+ error63.name = "AccessDeniedError";
73871
73917
  throw error63;
73872
73918
  }
73873
73919
  return { subject: payload2.sub };
73874
73920
  };
73875
73921
  }
73922
+ function permitsIdentity(policy, subject, claims) {
73923
+ if (policy.mode === "authenticated") return true;
73924
+ if (policy.mode === "subjects") return policy.subjects.includes(subject);
73925
+ const value = claims[policy.claim];
73926
+ const actual = typeof value === "string" && value.length > 0 ? [value] : Array.isArray(value) && value.every((item) => typeof item === "string" && item.length > 0) ? value : void 0;
73927
+ if (!actual) return false;
73928
+ return policy.match === "any" ? policy.values.some((expected) => actual.includes(expected)) : policy.values.every((expected) => actual.includes(expected));
73929
+ }
73876
73930
  async function fetchJson(url2, fetcher) {
73877
73931
  const response = await fetcher(url2);
73878
73932
  if (!response.ok) throw new Error(`OIDC discovery request failed: ${response.status}`);
@@ -73882,8 +73936,8 @@ async function fetchJson(url2, fetcher) {
73882
73936
  throw new Error("OIDC discovery did not return JSON.");
73883
73937
  }
73884
73938
  }
73885
- function unauthorizedSubject(error63) {
73886
- return error63 instanceof Error && error63.name === "UnauthorizedSubjectError";
73939
+ function accessDenied(error63) {
73940
+ return error63 instanceof Error && error63.name === "AccessDeniedError";
73887
73941
  }
73888
73942
 
73889
73943
  // packages/server/src/template.ts
@@ -73992,8 +74046,8 @@ function createApp(deps) {
73992
74046
  await deps.accessTokenVerifier(authorization.slice("Bearer ".length));
73993
74047
  } catch (error63) {
73994
74048
  return c5.json(
73995
- { error: unauthorizedSubject(error63) ? "Access denied." : "Invalid access token." },
73996
- unauthorizedSubject(error63) ? 403 : 401
74049
+ { error: accessDenied(error63) ? "Access denied." : "Invalid access token." },
74050
+ accessDenied(error63) ? 403 : 401
73997
74051
  );
73998
74052
  }
73999
74053
  return next();
@@ -74035,6 +74089,17 @@ function createApp(deps) {
74035
74089
  })
74036
74090
  });
74037
74091
  });
74092
+ app.get("/api/boards/:board/security", (c5) => {
74093
+ if (!deps.boardConfig?.boards[c5.req.param("board")] || !deps.boardConfig.auth)
74094
+ return c5.notFound();
74095
+ const policy = authorizationForAuth(deps.boardConfig.auth);
74096
+ return c5.json(
74097
+ securityDetailsResponseSchema.parse({
74098
+ oidc: true,
74099
+ policy: policy.mode === "claim" ? { mode: policy.mode, claim: policy.claim, match: policy.match } : { mode: policy.mode }
74100
+ })
74101
+ );
74102
+ });
74038
74103
  const layoutDownload = (c5, mode) => {
74039
74104
  const boardName = c5.req.param("board");
74040
74105
  const board = deps.boardConfig?.boards[boardName];
@@ -74055,8 +74120,7 @@ function createApp(deps) {
74055
74120
  const outputConfig = {
74056
74121
  sources,
74057
74122
  boards: { [boardName]: outputBoard },
74058
- ...deps.boardConfig.security ? { security: deps.boardConfig.security } : {},
74059
- ...deps.boardConfig.auth ? { auth: deps.boardConfig.auth } : {}
74123
+ ...deps.boardConfig.security ? { security: deps.boardConfig.security } : {}
74060
74124
  };
74061
74125
  const filenameBoard = safeDownloadFilename(boardName);
74062
74126
  return new Response(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@continuous-excellence/ze-great-dashboard-aws",
3
- "version": "0.30.6",
3
+ "version": "0.31.1",
4
4
  "type": "module",
5
5
  "description": "AWS server runtime and CloudFormation deployment tooling for Ze Great Dashboard.",
6
6
  "keywords": [