@uptime.app/contracts 3.0.45-rc.0 → 3.0.46
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/dist/cjs/acl/acl-utils.d.ts +38 -0
- package/dist/cjs/acl/acl-utils.d.ts.map +1 -0
- package/dist/cjs/acl/acl-utils.js +133 -0
- package/dist/cjs/acl/acl-utils.js.map +1 -0
- package/dist/cjs/collection/enriched-collection.d.ts +0 -2
- package/dist/cjs/collection/enriched-collection.d.ts.map +1 -1
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/acl/acl-utils.d.ts +38 -0
- package/dist/esm/acl/acl-utils.d.ts.map +1 -0
- package/dist/esm/acl/acl-utils.js +126 -0
- package/dist/esm/acl/acl-utils.js.map +1 -0
- package/dist/esm/collection/enriched-collection.d.ts +0 -2
- package/dist/esm/collection/enriched-collection.d.ts.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/package.json +2 -2
- package/src/access-control/collection-access-control.ts +28 -28
- package/src/access-control/hack-access-control.ts +32 -32
- package/src/access-control/profile-access-control.ts +27 -27
- package/src/acl/acl-utils.test.ts +272 -0
- package/src/acl/acl-utils.ts +179 -0
- package/src/app-hack/app-full-hack.ts +9 -9
- package/src/app-hack/app-hack-metadata.ts +61 -61
- package/src/app-hack/app-restricted-hack.ts +22 -22
- package/src/b2b-content-access/b2b-content-access.ts +57 -57
- package/src/b2b-organisation/b2b-carousel.ts +33 -33
- package/src/b2b-organisation/b2b-org-idp-mapping.ts +27 -27
- package/src/b2b-organisation/b2b-organisation-events.ts +21 -21
- package/src/b2b-organisation/b2b-organisation-group-events.ts +21 -21
- package/src/b2b-organisation/b2b-organisation-group-membership-events.ts +21 -21
- package/src/b2b-organisation/b2b-organisation-group-membership.ts +10 -10
- package/src/b2b-organisation/b2b-organisation-group.ts +16 -16
- package/src/b2b-organisation/b2b-organisation-invitation.ts +45 -45
- package/src/b2b-organisation/b2b-organisation-member-events.ts +21 -21
- package/src/b2b-organisation/b2b-organisation-member.ts +19 -19
- package/src/b2b-organisation/b2b-organisation.ts +38 -38
- package/src/brand/brand-events.ts +21 -21
- package/src/brand/brand.ts +77 -77
- package/src/catalogue/catalogue.ts +152 -152
- package/src/cms-hack/cms-searchable-hack.ts +44 -44
- package/src/collection/collection-events.ts +21 -21
- package/src/collection/collection.ts +59 -59
- package/src/collection/enriched-collection-events.ts +18 -18
- package/src/collection/enriched-collection.ts +63 -65
- package/src/course/course-events.ts +20 -20
- package/src/course/course.ts +25 -25
- package/src/course-enrollment/course-enrollment-events.ts +21 -21
- package/src/course-enrollment/course-enrollment.ts +36 -36
- package/src/creator/creator-events.ts +32 -32
- package/src/creator/creator.ts +7 -7
- package/src/distribution/collection-distribution.ts +17 -17
- package/src/distribution/distribution-events.ts +61 -61
- package/src/distribution/hack-distribution.ts +18 -18
- package/src/draft-hack/draft-hack-events.ts +21 -21
- package/src/draft-hack/draft-hack.ts +190 -190
- package/src/draft-hack-generation/draft-hack-generation-commands.ts +10 -10
- package/src/draft-hack-generation/draft-hack-generation-events.ts +52 -52
- package/src/draft-hack-generation/draft-hack-generation.ts +39 -39
- package/src/enriched-b2b-organisation/enriched-b2b-organisation-events.ts +21 -21
- package/src/enriched-b2b-organisation/enriched-b2b-organisation.ts +37 -37
- package/src/enriched-hack/enriched-hack-events.ts +21 -21
- package/src/enriched-hack/enriched-hack.ts +109 -109
- package/src/enrichment-asset/hack-enrichment-video-events.ts +55 -55
- package/src/enrichment-asset/hack-enrichment-video.ts +40 -40
- package/src/entitlement/user-entitlements-commands.ts +54 -54
- package/src/entitlement/user-entitlements-events.ts +29 -29
- package/src/entitlement/user-entitlements.ts +18 -18
- package/src/flashcard/flashcard-events.ts +85 -85
- package/src/flashcard/flashcard.ts +138 -138
- package/src/hack/extracted-subject.ts +31 -31
- package/src/hack/hack-events.ts +21 -21
- package/src/hack/hack-published.ts +21 -21
- package/src/hack/hack-with-voice-events.ts +20 -20
- package/src/hack/hack-with-voice.ts +36 -36
- package/src/hack/hack.ts +127 -127
- package/src/hack/publish-hack-command.ts +20 -20
- package/src/hack/republish-hack-command.ts +20 -20
- package/src/hack-mapping/access-helper.ts +6 -6
- package/src/hack-mapping/app-full-hack.test.ts +13 -12
- package/src/hack-mapping/app-full-hack.ts +14 -14
- package/src/hack-mapping/app-hack-metadata.test.ts +22 -22
- package/src/hack-mapping/app-hack-metadata.ts +82 -82
- package/src/hack-mapping/app-restricted-hack.test.ts +12 -11
- package/src/hack-mapping/app-restricted-hack.ts +21 -21
- package/src/hack-mapping/remove-undefined-fields.ts +9 -9
- package/src/hack-mapping/test-data/enriched-hack.json +2576 -2576
- package/src/hack-mapping/test-data/expected-app-full-hack.json +2509 -2509
- package/src/hack-mapping/test-data/expected-app-hack-metadata.json +93 -93
- package/src/hack-mapping/test-data/expected-app-restricted-hack.json +799 -799
- package/src/hack-metadata/hack-metadata-events.ts +21 -21
- package/src/hack-metadata/hack-metadata.ts +17 -17
- package/src/hack-of-the-day/hack-of-the-day-events.ts +21 -21
- package/src/hack-of-the-day/hack-of-the-day.ts +13 -13
- package/src/hack-statistics/hack-statistics-events.ts +21 -21
- package/src/hack-statistics/hack-statistics.ts +9 -9
- package/src/hackbot/hack-generation-commands.ts +45 -45
- package/src/hackbot/hack-generation-trigger-events.ts +21 -21
- package/src/hackbot/hack-generation-trigger.ts +57 -57
- package/src/history/history-events.ts +21 -21
- package/src/history/history.ts +46 -46
- package/src/identity-provider/domain-identity-provider-events.ts +20 -20
- package/src/identity-provider/domain-identity-provider.ts +10 -10
- package/src/identity-provider/identity-provider-events.ts +20 -20
- package/src/identity-provider/identity-provider.ts +46 -46
- package/src/index.ts +221 -219
- package/src/interest/interest-events.ts +21 -21
- package/src/interest/interest.ts +15 -15
- package/src/knowledge-source/knowledge-source-events.ts +21 -21
- package/src/knowledge-source/knowledge-source.ts +43 -43
- package/src/knowledge-source-parsing/knowledge-source-metadata.ts +58 -58
- package/src/knowledge-source-parsing/knowledge-source-parsing-commands.ts +41 -41
- package/src/knowledge-source-parsing/knowledge-source-parsing-events.ts +30 -30
- package/src/knowledge-source-parsing/podcast-metadata.ts +44 -44
- package/src/knowledge-source-parsing/podcast-parsing-events.ts +28 -28
- package/src/knowledge-source-parsing/user-hack-generation-options.ts +7 -7
- package/src/learn-upon/get-sqsso-url.ts +13 -13
- package/src/learn-upon/learn-upon-commands.ts +33 -33
- package/src/learn-upon/learn-upon-course-enrollment-events.ts +21 -21
- package/src/learn-upon/learn-upon-course-enrollment.ts +62 -62
- package/src/learn-upon/learn-upon-course-events.ts +21 -21
- package/src/learn-upon/learn-upon-course.ts +108 -108
- package/src/learn-upon/learn-upon-portal-events.ts +20 -20
- package/src/learn-upon/learn-upon-portal.ts +12 -12
- package/src/learn-upon/learn-upon-user-mapping-events.ts +18 -18
- package/src/learn-upon/learn-upon-user-mapping.ts +9 -9
- package/src/learn-upon/learn-upon-webhook-events.ts +80 -80
- package/src/learn-upon/learn-upon-webhook-types.ts +346 -346
- package/src/learn-upon/unenroll.ts +6 -6
- package/src/localisation/live-locales.ts +45 -45
- package/src/localisation/locale-language.ts +4 -4
- package/src/localisation/supported-languages.ts +11 -11
- package/src/localisation/supported-locales.ts +67 -67
- package/src/message-bus/context.ts +7 -7
- package/src/moderation/hack-moderation-result-events.ts +18 -18
- package/src/moderation/hack-moderation-result.ts +27 -27
- package/src/partner/partner-events.ts +21 -21
- package/src/partner/partner.ts +7 -7
- package/src/pending-hack/pending-hack-events.ts +20 -20
- package/src/pending-hack/pending-hack-with-voice-events.ts +20 -20
- package/src/pending-hack/pending-hack-with-voice.ts +25 -25
- package/src/pending-hack/pending-hack.ts +78 -78
- package/src/preferences/preferences-events.ts +15 -15
- package/src/preferences/preferences.ts +35 -35
- package/src/profile/profile-events.ts +41 -41
- package/src/profile/profile.ts +36 -36
- package/src/profile/translated-profile.ts +13 -13
- package/src/referral/referral-events.ts +41 -41
- package/src/referral/referral.ts +76 -76
- package/src/saved/saved-collection-events.ts +19 -19
- package/src/saved/saved-collection.ts +8 -8
- package/src/segment/segment-commands.ts +75 -75
- package/src/segment/segment-events.ts +42 -42
- package/src/segment/segment-with-voice.ts +8 -8
- package/src/segment/segment.ts +82 -82
- package/src/service-simulator/mock-request-response.ts +17 -17
- package/src/skin/skin-events.ts +21 -21
- package/src/skin/skin.ts +6 -6
- package/src/spark/spark-board-read-model.ts +17 -17
- package/src/spark/spark-board.ts +12 -12
- package/src/spark/spark-events.ts +42 -42
- package/src/spark/spark.ts +75 -75
- package/src/text-analysis/hack-categories-extracted-events.ts +24 -24
- package/src/text-analysis/hack-categories-extracted.ts +31 -31
- package/src/text-analysis/hack-entities-extracted-events.ts +24 -24
- package/src/text-analysis/hack-entities-extracted.ts +38 -38
- package/src/text-analysis/hack-keywords-extracted-events.ts +24 -24
- package/src/text-analysis/hack-keywords-extracted.ts +23 -23
- package/src/text-analysis/hack-topics-extracted-events.ts +24 -24
- package/src/text-analysis/hack-topics-extracted.ts +31 -31
- package/src/text-to-speech/create-speech-commands.ts +22 -22
- package/src/text-to-speech/speech-created-events.ts +10 -10
- package/src/text-to-speech/supported-voices.ts +222 -222
- package/src/text-to-speech/text-segment.ts +29 -29
- package/src/theme/theme-events.ts +21 -21
- package/src/theme/theme.ts +68 -68
- package/src/translation/translation-events.ts +73 -73
- package/src/translation/translation.ts +25 -25
- package/src/trending/trending-events.ts +21 -21
- package/src/trending/trending-hacks.ts +8 -8
- package/src/user/cognito-user-authentication-log.ts +7 -7
- package/src/user/get-user.ts +63 -63
- package/src/user/user-events.ts +39 -39
- package/src/user-context/user-context-events.ts +21 -21
- package/src/user-context/user-context.ts +15 -15
- package/src/user-notification-context/user-notification-context-events.ts +21 -21
- package/src/user-notification-context/user-notification-context.ts +9 -9
- package/src/user-statistics/user-statistics-events.ts +97 -97
- package/src/user-statistics/user-statistics.ts +82 -82
- package/src/video-cropping/video-cropping-events.ts +43 -43
- package/src/video-cropping/video-metadata.ts +12 -12
- package/tsconfig.cjs.json +7 -7
- package/tsconfig.npm.json +22 -22
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
export interface HackAccessControl {
|
|
2
|
-
id: string;
|
|
3
|
-
hackId: string; // PK
|
|
4
|
-
// do I need baseLocaleHackId ? don't think I do
|
|
5
|
-
v: number;
|
|
6
|
-
acl: string[]
|
|
7
|
-
createdAt: string;
|
|
8
|
-
updatedAt: string;
|
|
9
|
-
/** added April 2025 and backfilled */
|
|
10
|
-
owner: string;
|
|
11
|
-
tenant: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface HackAccessControlCreatedEvent {
|
|
15
|
-
id: string;
|
|
16
|
-
type: 'HackAccessControlCreated';
|
|
17
|
-
payload: HackAccessControl;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface HackAccessControlUpdatedEvent {
|
|
21
|
-
id: string;
|
|
22
|
-
type: 'HackAccessControlUpdated';
|
|
23
|
-
payload: HackAccessControl;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface HackAccessControlDeletedEvent {
|
|
27
|
-
id: string;
|
|
28
|
-
type: 'HackAccessControlDeleted';
|
|
29
|
-
payload: HackAccessControl;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type HackAccessControlChangedEvent = HackAccessControlCreatedEvent | HackAccessControlUpdatedEvent | HackAccessControlDeletedEvent;
|
|
1
|
+
export interface HackAccessControl {
|
|
2
|
+
id: string;
|
|
3
|
+
hackId: string; // PK
|
|
4
|
+
// do I need baseLocaleHackId ? don't think I do
|
|
5
|
+
v: number;
|
|
6
|
+
acl: string[]
|
|
7
|
+
createdAt: string;
|
|
8
|
+
updatedAt: string;
|
|
9
|
+
/** added April 2025 and backfilled */
|
|
10
|
+
owner: string;
|
|
11
|
+
tenant: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface HackAccessControlCreatedEvent {
|
|
15
|
+
id: string;
|
|
16
|
+
type: 'HackAccessControlCreated';
|
|
17
|
+
payload: HackAccessControl;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface HackAccessControlUpdatedEvent {
|
|
21
|
+
id: string;
|
|
22
|
+
type: 'HackAccessControlUpdated';
|
|
23
|
+
payload: HackAccessControl;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface HackAccessControlDeletedEvent {
|
|
27
|
+
id: string;
|
|
28
|
+
type: 'HackAccessControlDeleted';
|
|
29
|
+
payload: HackAccessControl;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type HackAccessControlChangedEvent = HackAccessControlCreatedEvent | HackAccessControlUpdatedEvent | HackAccessControlDeletedEvent;
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
export interface ProfileAccessControl {
|
|
2
|
-
id: string;
|
|
3
|
-
profileId: string;
|
|
4
|
-
v: number;
|
|
5
|
-
acl: string[]
|
|
6
|
-
createdAt: string;
|
|
7
|
-
updatedAt: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface ProfileAccessControlCreatedEvent {
|
|
11
|
-
id: string;
|
|
12
|
-
type: 'ProfileAccessControlCreated';
|
|
13
|
-
payload: ProfileAccessControl;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ProfileAccessControlUpdatedEvent {
|
|
17
|
-
id: string;
|
|
18
|
-
type: 'ProfileAccessControlUpdated';
|
|
19
|
-
payload: ProfileAccessControl;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ProfileAccessControlDeletedEvent {
|
|
23
|
-
id: string;
|
|
24
|
-
type: 'ProfileAccessControlDeleted';
|
|
25
|
-
payload: ProfileAccessControl;
|
|
26
|
-
}
|
|
27
|
-
|
|
1
|
+
export interface ProfileAccessControl {
|
|
2
|
+
id: string;
|
|
3
|
+
profileId: string;
|
|
4
|
+
v: number;
|
|
5
|
+
acl: string[]
|
|
6
|
+
createdAt: string;
|
|
7
|
+
updatedAt: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ProfileAccessControlCreatedEvent {
|
|
11
|
+
id: string;
|
|
12
|
+
type: 'ProfileAccessControlCreated';
|
|
13
|
+
payload: ProfileAccessControl;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProfileAccessControlUpdatedEvent {
|
|
17
|
+
id: string;
|
|
18
|
+
type: 'ProfileAccessControlUpdated';
|
|
19
|
+
payload: ProfileAccessControl;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ProfileAccessControlDeletedEvent {
|
|
23
|
+
id: string;
|
|
24
|
+
type: 'ProfileAccessControlDeleted';
|
|
25
|
+
payload: ProfileAccessControl;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
28
|
export type ProfileAccessControlChangedEvent = ProfileAccessControlCreatedEvent | ProfileAccessControlUpdatedEvent | ProfileAccessControlDeletedEvent;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { parseAclEntry, getAclValuesByNamespace, aclFor, AclBuilder } from './acl-utils';
|
|
2
|
+
|
|
3
|
+
describe('parseAclEntry', () => {
|
|
4
|
+
|
|
5
|
+
test('returns null for empty string', () => {
|
|
6
|
+
expect(parseAclEntry('')).toBeNull();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('returns null for whitespace-only string', () => {
|
|
10
|
+
expect(parseAclEntry(' ')).toBeNull();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('parses a simple namespace-only entry', () => {
|
|
14
|
+
expect(parseAclEntry('public')).toEqual({
|
|
15
|
+
raw: 'public',
|
|
16
|
+
excluded: false,
|
|
17
|
+
namespace: 'public',
|
|
18
|
+
value: null
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('parses a namespace:value entry', () => {
|
|
23
|
+
expect(parseAclEntry('catalogue:uptime')).toEqual({
|
|
24
|
+
raw: 'catalogue:uptime',
|
|
25
|
+
excluded: false,
|
|
26
|
+
namespace: 'catalogue',
|
|
27
|
+
value: 'uptime'
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('parses an excluded namespace-only entry', () => {
|
|
32
|
+
expect(parseAclEntry('!public')).toEqual({
|
|
33
|
+
raw: '!public',
|
|
34
|
+
excluded: true,
|
|
35
|
+
namespace: 'public',
|
|
36
|
+
value: null
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('parses an excluded namespace:value entry', () => {
|
|
41
|
+
expect(parseAclEntry('!catalogue:uptime')).toEqual({
|
|
42
|
+
raw: '!catalogue:uptime',
|
|
43
|
+
excluded: true,
|
|
44
|
+
namespace: 'catalogue',
|
|
45
|
+
value: 'uptime'
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('trims leading and trailing whitespace', () => {
|
|
50
|
+
expect(parseAclEntry(' catalogue:uptime ')).toEqual({
|
|
51
|
+
raw: 'catalogue:uptime',
|
|
52
|
+
excluded: false,
|
|
53
|
+
namespace: 'catalogue',
|
|
54
|
+
value: 'uptime'
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('returns null when namespace is empty (entry starts with colon)', () => {
|
|
59
|
+
expect(parseAclEntry(':value')).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('preserves colons in value', () => {
|
|
63
|
+
expect(parseAclEntry('catalogue:b2b:org-123')).toEqual({
|
|
64
|
+
raw: 'catalogue:b2b:org-123',
|
|
65
|
+
excluded: false,
|
|
66
|
+
namespace: 'catalogue',
|
|
67
|
+
value: 'b2b:org-123'
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('getAclValuesByNamespace', () => {
|
|
73
|
+
const acl = [
|
|
74
|
+
'public',
|
|
75
|
+
'catalogue:uptime',
|
|
76
|
+
'catalogue:premium',
|
|
77
|
+
'!catalogue:restricted',
|
|
78
|
+
'b2b:org-1',
|
|
79
|
+
'!b2b:org-2',
|
|
80
|
+
'user:user-123'
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
test('returns included values for a namespace by default', () => {
|
|
84
|
+
expect(getAclValuesByNamespace(acl, 'catalogue')).toEqual(['uptime', 'premium']);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('returns excluded values when mode is "excluded"', () => {
|
|
88
|
+
expect(getAclValuesByNamespace(acl, 'catalogue', { mode: 'excluded' })).toEqual(['restricted']);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('returns all values when mode is "all"', () => {
|
|
92
|
+
expect(getAclValuesByNamespace(acl, 'catalogue', { mode: 'all' })).toEqual(['uptime', 'premium', 'restricted']);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('returns empty array when namespace has no matches', () => {
|
|
96
|
+
expect(getAclValuesByNamespace(acl, 'subject')).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('deduplicates values by default', () => {
|
|
100
|
+
const aclWithDupes = ['catalogue:uptime', 'catalogue:uptime', 'catalogue:premium'];
|
|
101
|
+
expect(getAclValuesByNamespace(aclWithDupes, 'catalogue')).toEqual(['uptime', 'premium']);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('preserves duplicates when dedupe is false', () => {
|
|
105
|
+
const aclWithDupes = ['catalogue:uptime', 'catalogue:uptime', 'catalogue:premium'];
|
|
106
|
+
expect(getAclValuesByNamespace(aclWithDupes, 'catalogue', { dedupe: false })).toEqual(['uptime', 'uptime', 'premium']);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('excludes namespace-only entries (no value)', () => {
|
|
110
|
+
expect(getAclValuesByNamespace(acl, 'public')).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('returns empty array for empty acl', () => {
|
|
114
|
+
expect(getAclValuesByNamespace([], 'catalogue')).toEqual([]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('skips invalid entries', () => {
|
|
118
|
+
const aclWithInvalid = ['', ' ', 'catalogue:uptime'];
|
|
119
|
+
expect(getAclValuesByNamespace(aclWithInvalid, 'catalogue')).toEqual(['uptime']);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('AclBuilder', () => {
|
|
124
|
+
test('includes "public" by default', () => {
|
|
125
|
+
const acl = new AclBuilder().build();
|
|
126
|
+
expect(acl).toContain('public');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('excludes "public" when includePublic is false', () => {
|
|
130
|
+
const acl = new AclBuilder({ includePublic: false }).build();
|
|
131
|
+
expect(acl).not.toContain('public');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('includes user entry when includeUser is specified', () => {
|
|
135
|
+
const acl = new AclBuilder({ includeUser: 'user-123' }).build();
|
|
136
|
+
expect(acl).toContain('user:user-123');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('merges baseAcl entries', () => {
|
|
140
|
+
const acl = new AclBuilder({ baseAcl: ['catalogue:uptime', '!catalogue:restricted'] }).build();
|
|
141
|
+
expect(acl).toContain('catalogue:uptime');
|
|
142
|
+
expect(acl).toContain('!catalogue:restricted');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('allow', () => {
|
|
146
|
+
test('adds namespace:value entry', () => {
|
|
147
|
+
const acl = new AclBuilder({ includePublic: false }).allow('catalogue', 'uptime').build();
|
|
148
|
+
expect(acl).toContain('catalogue:uptime');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('adds namespace-only entry when no value provided', () => {
|
|
152
|
+
const acl = new AclBuilder({ includePublic: false }).allow('public').build();
|
|
153
|
+
expect(acl).toContain('public');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('removes entry from excluded when allowed', () => {
|
|
157
|
+
const acl = new AclBuilder({ includePublic: false })
|
|
158
|
+
.deny('catalogue', 'uptime')
|
|
159
|
+
.allow('catalogue', 'uptime')
|
|
160
|
+
.build();
|
|
161
|
+
expect(acl).toContain('catalogue:uptime');
|
|
162
|
+
expect(acl).not.toContain('!catalogue:uptime');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('deny', () => {
|
|
167
|
+
test('adds excluded namespace:value entry', () => {
|
|
168
|
+
const acl = new AclBuilder().deny('catalogue', 'restricted').build();
|
|
169
|
+
expect(acl).toContain('!catalogue:restricted');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('removes entry from included when denied', () => {
|
|
173
|
+
const acl = new AclBuilder({ includePublic: false })
|
|
174
|
+
.allow('catalogue', 'uptime')
|
|
175
|
+
.deny('catalogue', 'uptime')
|
|
176
|
+
.build();
|
|
177
|
+
expect(acl).not.toContain('catalogue:uptime');
|
|
178
|
+
expect(acl).toContain('!catalogue:uptime');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('allowCatalogue', () => {
|
|
183
|
+
test('adds catalogue:<id> entry', () => {
|
|
184
|
+
const acl = new AclBuilder().allowCatalogue('premium').build();
|
|
185
|
+
expect(acl).toContain('catalogue:premium');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('denyCatalogue', () => {
|
|
190
|
+
test('adds !catalogue:<id> entry', () => {
|
|
191
|
+
const acl = new AclBuilder().denyCatalogue('restricted').build();
|
|
192
|
+
expect(acl).toContain('!catalogue:restricted');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('allowOrganisation', () => {
|
|
197
|
+
test('adds b2b:<id> entry', () => {
|
|
198
|
+
const acl = new AclBuilder().allowOrganisation('org-1').build();
|
|
199
|
+
expect(acl).toContain('b2b:org-1');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('denySubject', () => {
|
|
204
|
+
test('adds !subject:<value> entry', () => {
|
|
205
|
+
const acl = new AclBuilder().denySubject('some-topic').build();
|
|
206
|
+
expect(acl).toContain('!subject:some-topic');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('addRaw', () => {
|
|
211
|
+
test('adds an included raw entry', () => {
|
|
212
|
+
const acl = new AclBuilder({ includePublic: false }).addRaw('catalogue:uptime').build();
|
|
213
|
+
expect(acl).toContain('catalogue:uptime');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('adds an excluded raw entry', () => {
|
|
217
|
+
const acl = new AclBuilder({ includePublic: false }).addRaw('!catalogue:restricted').build();
|
|
218
|
+
expect(acl).toContain('!catalogue:restricted');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('ignores invalid raw entries', () => {
|
|
222
|
+
const acl = new AclBuilder({ includePublic: false }).addRaw('').build();
|
|
223
|
+
expect(acl).toEqual([]);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('addMany', () => {
|
|
228
|
+
test('adds multiple entries at once', () => {
|
|
229
|
+
const acl = new AclBuilder({ includePublic: false })
|
|
230
|
+
.addMany(['catalogue:uptime', 'b2b:org-1', '!catalogue:restricted'])
|
|
231
|
+
.build();
|
|
232
|
+
expect(acl).toContain('catalogue:uptime');
|
|
233
|
+
expect(acl).toContain('b2b:org-1');
|
|
234
|
+
expect(acl).toContain('!catalogue:restricted');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('aclFor', () => {
|
|
240
|
+
test('includes user entry', () => {
|
|
241
|
+
const acl = aclFor({ userId: 'user-abc', organisationIds: [], catalogueIds: [] });
|
|
242
|
+
expect(acl).toContain('user:user-abc');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('always includes catalogue:uptime', () => {
|
|
246
|
+
const acl = aclFor({ userId: 'user-abc', organisationIds: [], catalogueIds: [] });
|
|
247
|
+
expect(acl).toContain('catalogue:uptime');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('includes provided catalogue ids', () => {
|
|
251
|
+
const acl = aclFor({ userId: 'user-abc', organisationIds: [], catalogueIds: ['premium', 'free'] });
|
|
252
|
+
expect(acl).toContain('catalogue:premium');
|
|
253
|
+
expect(acl).toContain('catalogue:free');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('includes b2b catalogue entry for each organisation', () => {
|
|
257
|
+
const acl = aclFor({ userId: 'user-abc', organisationIds: ['org-1', 'org-2'], catalogueIds: [] });
|
|
258
|
+
expect(acl).toContain('catalogue:b2b:org-1');
|
|
259
|
+
expect(acl).toContain('catalogue:b2b:org-2');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('includes b2b namespace entry for each organisation', () => {
|
|
263
|
+
const acl = aclFor({ userId: 'user-abc', organisationIds: ['org-1', 'org-2'], catalogueIds: [] });
|
|
264
|
+
expect(acl).toContain('b2b:org-1');
|
|
265
|
+
expect(acl).toContain('b2b:org-2');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('includes public by default', () => {
|
|
269
|
+
const acl = aclFor({ userId: 'user-abc', organisationIds: [], catalogueIds: [] });
|
|
270
|
+
expect(acl).toContain('public');
|
|
271
|
+
});
|
|
272
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
export type AclQueryMode = 'included' | 'excluded' | 'all';
|
|
2
|
+
|
|
3
|
+
export interface ParsedAclEntry {
|
|
4
|
+
raw: string;
|
|
5
|
+
excluded: boolean;
|
|
6
|
+
namespace: string;
|
|
7
|
+
value: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface GetAclValuesOptions {
|
|
11
|
+
mode?: AclQueryMode;
|
|
12
|
+
dedupe?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface BuildAclOptions {
|
|
16
|
+
includePublic?: boolean;
|
|
17
|
+
includeUser?: string;
|
|
18
|
+
baseAcl?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeAclEntry(entry: string): string {
|
|
22
|
+
return entry.trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parseAclEntry(entry: string): ParsedAclEntry | null {
|
|
26
|
+
const raw = normalizeAclEntry(entry);
|
|
27
|
+
if (!raw) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const excluded = raw.startsWith('!');
|
|
32
|
+
const token = excluded ? raw.slice(1) : raw;
|
|
33
|
+
const firstColonIndex = token.indexOf(':');
|
|
34
|
+
|
|
35
|
+
if (firstColonIndex === -1) {
|
|
36
|
+
return {
|
|
37
|
+
raw,
|
|
38
|
+
excluded,
|
|
39
|
+
namespace: token,
|
|
40
|
+
value: null
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const namespace = token.slice(0, firstColonIndex);
|
|
45
|
+
const value = token.slice(firstColonIndex + 1);
|
|
46
|
+
|
|
47
|
+
if (!namespace) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
raw,
|
|
53
|
+
excluded,
|
|
54
|
+
namespace,
|
|
55
|
+
value
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getAclValuesByNamespace(
|
|
60
|
+
acl: string[],
|
|
61
|
+
namespace: string,
|
|
62
|
+
options: GetAclValuesOptions = {}
|
|
63
|
+
): string[] {
|
|
64
|
+
const mode = options.mode ?? 'included';
|
|
65
|
+
const dedupe = options.dedupe ?? true;
|
|
66
|
+
|
|
67
|
+
const values = acl
|
|
68
|
+
.map(parseAclEntry)
|
|
69
|
+
.filter((entry): entry is ParsedAclEntry => Boolean(entry))
|
|
70
|
+
.filter(entry => entry.namespace === namespace)
|
|
71
|
+
.filter(entry => entry.value !== null)
|
|
72
|
+
.filter(entry => {
|
|
73
|
+
if (mode === 'all') {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
if (mode === 'included') {
|
|
77
|
+
return !entry.excluded;
|
|
78
|
+
}
|
|
79
|
+
return entry.excluded;
|
|
80
|
+
})
|
|
81
|
+
.map(entry => entry.value as string);
|
|
82
|
+
|
|
83
|
+
return dedupe ? Array.from(new Set(values)) : values;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function aclFor({ userId, organisationIds, catalogueIds }: { userId: string; organisationIds: string[]; catalogueIds: string[] }) {
|
|
87
|
+
const builder = new AclBuilder({ includeUser: userId });
|
|
88
|
+
|
|
89
|
+
// everyone has access to the uptime catalogue
|
|
90
|
+
builder.allowCatalogue('uptime');
|
|
91
|
+
|
|
92
|
+
catalogueIds.forEach(catalogueId => builder.allowCatalogue(catalogueId));
|
|
93
|
+
|
|
94
|
+
// each b2b has a catalogue called b2b:<organisationId> so permit access to this
|
|
95
|
+
organisationIds.forEach(organisationId => builder.allowCatalogue(`b2b:${organisationId}`));
|
|
96
|
+
|
|
97
|
+
// previously we also had acl of b2b:<organisationId> that is still being used for collections
|
|
98
|
+
organisationIds.forEach(organisationId => builder.allowOrganisation(organisationId));
|
|
99
|
+
|
|
100
|
+
return builder.build();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class AclBuilder {
|
|
104
|
+
private included = new Set<string>();
|
|
105
|
+
private excluded = new Set<string>();
|
|
106
|
+
|
|
107
|
+
constructor(options: BuildAclOptions = {}) {
|
|
108
|
+
const includePublic = options.includePublic ?? true;
|
|
109
|
+
|
|
110
|
+
if (includePublic) {
|
|
111
|
+
this.included.add('public');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (options.includeUser) {
|
|
115
|
+
this.included.add(`user:${options.includeUser}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (options.baseAcl?.length) {
|
|
119
|
+
this.addMany(options.baseAcl);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
addRaw(entry: string): this {
|
|
124
|
+
const parsed = parseAclEntry(entry);
|
|
125
|
+
if (!parsed) {
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (parsed.excluded) {
|
|
130
|
+
this.excluded.add(parsed.raw.slice(1));
|
|
131
|
+
} else {
|
|
132
|
+
this.included.add(parsed.raw);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
addMany(entries: string[]): this {
|
|
139
|
+
entries.forEach(entry => this.addRaw(entry));
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
allow(namespace: string, value?: string): this {
|
|
144
|
+
const token = value === undefined ? namespace : `${namespace}:${value}`;
|
|
145
|
+
this.included.add(token);
|
|
146
|
+
this.excluded.delete(token);
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
deny(namespace: string, value?: string): this {
|
|
151
|
+
const token = value === undefined ? namespace : `${namespace}:${value}`;
|
|
152
|
+
this.excluded.add(token);
|
|
153
|
+
this.included.delete(token);
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
allowCatalogue(catalogueId: string): this {
|
|
158
|
+
return this.allow('catalogue', catalogueId);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
denyCatalogue(catalogueId: string): this {
|
|
162
|
+
return this.deny('catalogue', catalogueId);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
allowOrganisation(organisationId: string): this {
|
|
166
|
+
return this.allow('b2b', organisationId);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
denySubject(subject: string): this {
|
|
170
|
+
return this.deny('subject', subject);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
build(): string[] {
|
|
174
|
+
return [
|
|
175
|
+
...this.included,
|
|
176
|
+
...Array.from(this.excluded).map(token => `!${token}`)
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { AppRestrictedHack } from './app-restricted-hack';
|
|
2
|
-
import { EnrichedHack } from '../enriched-hack/enriched-hack';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Includes all of a hack's content that premium users are allowed to access. Free users should not have access to this object.
|
|
6
|
-
*/
|
|
7
|
-
export interface AppFullHack extends AppRestrictedHack {
|
|
8
|
-
segments: EnrichedHack['segments'];
|
|
9
|
-
}
|
|
1
|
+
import { AppRestrictedHack } from './app-restricted-hack';
|
|
2
|
+
import { EnrichedHack } from '../enriched-hack/enriched-hack';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Includes all of a hack's content that premium users are allowed to access. Free users should not have access to this object.
|
|
6
|
+
*/
|
|
7
|
+
export interface AppFullHack extends AppRestrictedHack {
|
|
8
|
+
segments: EnrichedHack['segments'];
|
|
9
|
+
}
|