@guardian/permissions-client 0.0.0-canary-20260722094505

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 ADDED
@@ -0,0 +1,7 @@
1
+ # @guardian/permissions-client
2
+
3
+ ## 0.0.0-canary-20260722094505
4
+
5
+ ### Patch Changes
6
+
7
+ - fbf5e70: Initial release for @guardian/permissions-client
package/LICENSE ADDED
@@ -0,0 +1,8 @@
1
+ This software is licensed under the Apache 2 license, quoted below.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with
4
+ the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
5
+
6
+ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
7
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
8
+ language governing permissions and limitations under the License.
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@guardian/permissions-client",
3
+ "version": "0.0.0-canary-20260722094505",
4
+ "description": "Client for Guardian's permissions service",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./dist/index.js",
9
+ "types": "./dist/declaration/index.d.ts"
10
+ }
11
+ },
12
+ "devDependencies": {
13
+ "@aws-sdk/client-s3": "3.1090.0",
14
+ "@aws-sdk/credential-providers": "^3.1090.0"
15
+ },
16
+ "peerDependencies": {
17
+ "@aws-sdk/client-s3": "^3.1090.0",
18
+ "@aws-sdk/credential-providers": "^3.1090.0"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "test": "echo \"Error: no test specified\" && exit 1",
23
+ "format": "prettier --write \"src/**/*.ts\"",
24
+ "format:ci": "prettier --check \"src/**/*.ts\"",
25
+ "lint": "eslint src/** --ext .ts"
26
+ }
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,124 @@
1
+ import { S3 } from "@aws-sdk/client-s3";
2
+ import { fromIni, fromNodeProviderChain } from "@aws-sdk/credential-providers";
3
+ import type { PermissionsCache, PermissionWithUsers } from "./model";
4
+
5
+ interface ClientConfig {
6
+ stage?: string;
7
+ region?: string;
8
+ bucket?: string;
9
+ file?: string;
10
+ cacheIntervalMs?: number;
11
+ isRunningLocally?: boolean;
12
+ localProfile?: string;
13
+ }
14
+
15
+ type PermissionsResult = {
16
+ allPermissions: PermissionsCache;
17
+ loadTime: number | null;
18
+ };
19
+
20
+ let permissionsResult: PermissionsResult = {
21
+ allPermissions: [],
22
+ loadTime: null,
23
+ };
24
+ let permissionsRequest: Promise<PermissionsResult> =
25
+ Promise.resolve(permissionsResult);
26
+
27
+ const permissionValidForUser = (
28
+ permission: PermissionWithUsers,
29
+ userId: string,
30
+ ) => {
31
+ const userOverride = permission.overrides.find((o) => o.userId === userId);
32
+
33
+ return !!(userOverride?.active && !userOverride.isCasualNotOnShift);
34
+ };
35
+
36
+ export function init({
37
+ stage = "CODE",
38
+ region = "eu-west-1",
39
+ bucket = "permissions-cache",
40
+ file = "permissions.json",
41
+ cacheIntervalMs = 60_000, // 1 minute,
42
+ isRunningLocally = false,
43
+ localProfile = "workflow",
44
+ }: ClientConfig = {}) {
45
+ const awsConfig = {
46
+ region,
47
+ credentials: isRunningLocally
48
+ ? fromIni({ profile: localProfile })
49
+ : fromNodeProviderChain(),
50
+ };
51
+
52
+ const s3 = new S3(awsConfig);
53
+
54
+ async function loadPermissions() {
55
+ const { Body } = await s3.getObject({
56
+ Bucket: bucket,
57
+ Key: `${stage}/${file}`,
58
+ });
59
+
60
+ if (!Body) {
61
+ throw Error("Could not fetch permissions");
62
+ }
63
+
64
+ const transformedBody = await Body.transformToString();
65
+ const allPermissions = JSON.parse(transformedBody) as PermissionsCache;
66
+
67
+ return { allPermissions, loadTime: Date.now() };
68
+ }
69
+
70
+ async function getOrRefresh() {
71
+ const latestResult = await permissionsRequest;
72
+ const cacheExpired = latestResult.loadTime
73
+ ? Date.now() > latestResult.loadTime + cacheIntervalMs
74
+ : true;
75
+
76
+ if (cacheExpired) {
77
+ try {
78
+ permissionsRequest = loadPermissions();
79
+ permissionsResult = await permissionsRequest;
80
+ } catch (error) {
81
+ console.error(
82
+ "Refreshing permissions cache failed with error, so keeping stale cache...",
83
+ error,
84
+ );
85
+ }
86
+ }
87
+
88
+ return permissionsResult.allPermissions;
89
+ }
90
+
91
+ async function hasPermission(permissionName: string, userId: string) {
92
+ const permissionsCache = await getOrRefresh();
93
+ const permissionData = permissionsCache.find(
94
+ (p) => p.permission.name === permissionName,
95
+ );
96
+
97
+ if (!permissionData) {
98
+ throw Error(`Permission data for "${permissionName}" not found`);
99
+ }
100
+
101
+ return permissionValidForUser(permissionData, userId);
102
+ }
103
+
104
+ async function listPermissions(userId: string) {
105
+ const permissionsCache = await getOrRefresh();
106
+
107
+ return permissionsCache
108
+ .filter((p) => permissionValidForUser(p, userId))
109
+ .map((p) => p.permission.name);
110
+ }
111
+
112
+ function storeIsEmpty() {
113
+ return permissionsResult.loadTime === null;
114
+ }
115
+
116
+ // Run getOrRefresh() immediately after init so that the cache is populated and ready for use
117
+ void getOrRefresh();
118
+
119
+ return {
120
+ hasPermission,
121
+ listPermissions,
122
+ storeIsEmpty,
123
+ };
124
+ }
package/src/model.ts ADDED
@@ -0,0 +1,20 @@
1
+ // TODO: should we have runtime type checking with zod, or just trust what's coming back from the bucket?
2
+
3
+ interface UserSetting {
4
+ userId: string;
5
+ active: boolean;
6
+ isCasualNotOnShift: boolean;
7
+ }
8
+
9
+ interface PermissionDefinition {
10
+ name: string;
11
+ app: string;
12
+ defaultValue: boolean;
13
+ }
14
+
15
+ export interface PermissionWithUsers {
16
+ permission: PermissionDefinition;
17
+ overrides: UserSetting[];
18
+ }
19
+
20
+ export type PermissionsCache = PermissionWithUsers[];
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "@guardian/tsconfig/tsconfig.json",
3
+ "allowImportingTsExtensions": true,
4
+ "compilerOptions": {
5
+ "module": "esnext",
6
+ "types": [
7
+ "vitest/globals",
8
+ "node"
9
+ ],
10
+ "declaration": true,
11
+ "rootDir": "./src",
12
+ "outDir": "dist",
13
+ "declarationDir": "dist/declaration",
14
+ },
15
+ "include": ["src/**/*"],
16
+ }