@daltonr/authwrite-hateoas 0.1.0
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/index.d.ts +80 -0
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -0
- package/src/index.ts +142 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { AuthEvaluator, Subject, Resource, Action } from '@daltonr/authwrite-core';
|
|
2
|
+
export interface LinkTemplate {
|
|
3
|
+
/** The URL for this action. May be a plain string or a pre-resolved href. */
|
|
4
|
+
href: string;
|
|
5
|
+
/** HTTP method. Defaults to 'GET'. */
|
|
6
|
+
method?: string;
|
|
7
|
+
/** Human-readable label, useful for UI rendering. */
|
|
8
|
+
title?: string;
|
|
9
|
+
/** Any additional properties (templated, type, etc.) */
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
/** A map of action name → link. Only permitted actions are present. */
|
|
13
|
+
export type LinkMap = Record<string, LinkTemplate>;
|
|
14
|
+
export interface BuildLinksConfig<S extends Subject = Subject, R extends Resource = Resource> {
|
|
15
|
+
engine: AuthEvaluator<S, R>;
|
|
16
|
+
subject: S;
|
|
17
|
+
resource?: R;
|
|
18
|
+
/**
|
|
19
|
+
* Map of action name → link template.
|
|
20
|
+
* Only entries whose action is permitted will appear in the result.
|
|
21
|
+
*/
|
|
22
|
+
actions: Record<Action, LinkTemplate>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Evaluates every action in `config.actions` and returns a `LinkMap`
|
|
26
|
+
* containing only the links the subject is permitted to follow.
|
|
27
|
+
*
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const links = await buildLinks({
|
|
30
|
+
* engine,
|
|
31
|
+
* subject,
|
|
32
|
+
* resource,
|
|
33
|
+
* actions: {
|
|
34
|
+
* read: { href: `/documents/${id}`, method: 'GET' },
|
|
35
|
+
* write: { href: `/documents/${id}`, method: 'PUT' },
|
|
36
|
+
* delete: { href: `/documents/${id}`, method: 'DELETE' },
|
|
37
|
+
* archive: { href: `/documents/${id}/archive`, method: 'POST' },
|
|
38
|
+
* },
|
|
39
|
+
* })
|
|
40
|
+
* // → only links whose policy decision was allowed: true
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildLinks<S extends Subject = Subject, R extends Resource = Resource>(config: BuildLinksConfig<S, R>): Promise<LinkMap>;
|
|
44
|
+
export interface EmbedLinksConfig<S extends Subject = Subject, R extends Resource = Resource> extends BuildLinksConfig<S, R> {
|
|
45
|
+
/**
|
|
46
|
+
* A `self` link added unconditionally — it is not subject to policy
|
|
47
|
+
* evaluation since the resource has already been fetched and returned.
|
|
48
|
+
*/
|
|
49
|
+
self?: LinkTemplate;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Builds permission-aware links and merges them into `data` as a `_links`
|
|
53
|
+
* property, following HAL (application/hal+json) conventions.
|
|
54
|
+
*
|
|
55
|
+
* ```typescript
|
|
56
|
+
* const body = await embedLinks(document, {
|
|
57
|
+
* engine, subject, resource: document,
|
|
58
|
+
* self: { href: `/documents/${document.id}`, method: 'GET' },
|
|
59
|
+
* actions: {
|
|
60
|
+
* write: { href: `/documents/${document.id}`, method: 'PUT' },
|
|
61
|
+
* delete: { href: `/documents/${document.id}`, method: 'DELETE' },
|
|
62
|
+
* },
|
|
63
|
+
* })
|
|
64
|
+
* // → { ...document, _links: { self: {...}, write: {...} } }
|
|
65
|
+
* // (delete absent — not permitted)
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function embedLinks<T extends object, S extends Subject = Subject, R extends Resource = Resource>(data: T, config: EmbedLinksConfig<S, R>): Promise<T & {
|
|
69
|
+
_links: LinkMap;
|
|
70
|
+
}>;
|
|
71
|
+
/**
|
|
72
|
+
* Synchronous variant for cases where you have already called `engine.permissions()`
|
|
73
|
+
* and want to build links without an additional async round-trip.
|
|
74
|
+
*
|
|
75
|
+
* ```typescript
|
|
76
|
+
* const perms = await engine.permissions(subject, resource, ['read', 'write', 'delete'])
|
|
77
|
+
* const links = linksFromDecisions(perms, actionTemplates)
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export declare function linksFromDecisions(permissions: Record<string, boolean>, actions: Record<Action, LinkTemplate>): LinkMap;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// ─── buildLinks ───────────────────────────────────────────────────────────────
|
|
2
|
+
/**
|
|
3
|
+
* Evaluates every action in `config.actions` and returns a `LinkMap`
|
|
4
|
+
* containing only the links the subject is permitted to follow.
|
|
5
|
+
*
|
|
6
|
+
* ```typescript
|
|
7
|
+
* const links = await buildLinks({
|
|
8
|
+
* engine,
|
|
9
|
+
* subject,
|
|
10
|
+
* resource,
|
|
11
|
+
* actions: {
|
|
12
|
+
* read: { href: `/documents/${id}`, method: 'GET' },
|
|
13
|
+
* write: { href: `/documents/${id}`, method: 'PUT' },
|
|
14
|
+
* delete: { href: `/documents/${id}`, method: 'DELETE' },
|
|
15
|
+
* archive: { href: `/documents/${id}/archive`, method: 'POST' },
|
|
16
|
+
* },
|
|
17
|
+
* })
|
|
18
|
+
* // → only links whose policy decision was allowed: true
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export async function buildLinks(config) {
|
|
22
|
+
const actionNames = Object.keys(config.actions);
|
|
23
|
+
const permitted = config.resource !== undefined
|
|
24
|
+
? await config.engine.permissions(config.subject, config.resource, actionNames)
|
|
25
|
+
: await config.engine.permissions(config.subject, actionNames);
|
|
26
|
+
const links = {};
|
|
27
|
+
for (const action of actionNames) {
|
|
28
|
+
if (permitted[action]) {
|
|
29
|
+
links[action] = config.actions[action];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return links;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Builds permission-aware links and merges them into `data` as a `_links`
|
|
36
|
+
* property, following HAL (application/hal+json) conventions.
|
|
37
|
+
*
|
|
38
|
+
* ```typescript
|
|
39
|
+
* const body = await embedLinks(document, {
|
|
40
|
+
* engine, subject, resource: document,
|
|
41
|
+
* self: { href: `/documents/${document.id}`, method: 'GET' },
|
|
42
|
+
* actions: {
|
|
43
|
+
* write: { href: `/documents/${document.id}`, method: 'PUT' },
|
|
44
|
+
* delete: { href: `/documents/${document.id}`, method: 'DELETE' },
|
|
45
|
+
* },
|
|
46
|
+
* })
|
|
47
|
+
* // → { ...document, _links: { self: {...}, write: {...} } }
|
|
48
|
+
* // (delete absent — not permitted)
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export async function embedLinks(data, config) {
|
|
52
|
+
const permitted = await buildLinks(config);
|
|
53
|
+
const _links = {};
|
|
54
|
+
if (config.self)
|
|
55
|
+
_links['self'] = config.self;
|
|
56
|
+
Object.assign(_links, permitted);
|
|
57
|
+
return { ...data, _links };
|
|
58
|
+
}
|
|
59
|
+
// ─── Decision-based link filtering (sync, from pre-fetched decisions) ─────────
|
|
60
|
+
/**
|
|
61
|
+
* Synchronous variant for cases where you have already called `engine.permissions()`
|
|
62
|
+
* and want to build links without an additional async round-trip.
|
|
63
|
+
*
|
|
64
|
+
* ```typescript
|
|
65
|
+
* const perms = await engine.permissions(subject, resource, ['read', 'write', 'delete'])
|
|
66
|
+
* const links = linksFromDecisions(perms, actionTemplates)
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function linksFromDecisions(permissions, actions) {
|
|
70
|
+
const links = {};
|
|
71
|
+
for (const [action, template] of Object.entries(actions)) {
|
|
72
|
+
if (permissions[action]) {
|
|
73
|
+
links[action] = template;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return links;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkCA,iFAAiF;AAEjF;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAG9B,MAA8B;IAC9B,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAE/C,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,KAAK,SAAS;QAC7C,CAAC,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC;QAC/E,CAAC,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC,CAAA;IAEhE,MAAM,KAAK,GAAY,EAAE,CAAA;IACzB,KAAK,MAAM,MAAM,IAAI,WAAW,EAAE,CAAC;QACjC,IAAI,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;YACtB,KAAK,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QACxC,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC;AAeD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAI9B,IAAO,EAAE,MAA8B;IACvC,MAAM,SAAS,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,CAAA;IAE1C,MAAM,MAAM,GAAY,EAAE,CAAA;IAC1B,IAAI,MAAM,CAAC,IAAI;QAAE,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAA;IAC7C,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IAEhC,OAAO,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,CAAA;AAC5B,CAAC;AAED,iFAAiF;AAEjF;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAChC,WAAoC,EACpC,OAAyC;IAEzC,MAAM,KAAK,GAAY,EAAE,CAAA;IACzB,KAAK,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACzD,IAAI,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;YACxB,KAAK,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAA;QAC1B,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@daltonr/authwrite-hateoas",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "HATEOAS link building for Authwrite — permission-aware hypermedia links from policy decisions.",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/richardadalton/authwrite.git",
|
|
10
|
+
"directory": "packages/hateoas"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["authorization", "authz", "hateoas", "hal", "hypermedia", "links"],
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"main": "dist/index.js",
|
|
21
|
+
"types": "dist/index.d.ts",
|
|
22
|
+
"files": ["dist", "src", "README.md", "LICENSE"],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.json",
|
|
25
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
26
|
+
"prepublishOnly": "test -d dist && echo 'dist already built, skipping' || (npm run clean && npm run build)"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@daltonr/authwrite-core": "*"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { AuthEvaluator, Subject, Resource, Action } from '@daltonr/authwrite-core'
|
|
2
|
+
|
|
3
|
+
// ─── Link types ───────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface LinkTemplate {
|
|
6
|
+
/** The URL for this action. May be a plain string or a pre-resolved href. */
|
|
7
|
+
href: string
|
|
8
|
+
/** HTTP method. Defaults to 'GET'. */
|
|
9
|
+
method?: string
|
|
10
|
+
/** Human-readable label, useful for UI rendering. */
|
|
11
|
+
title?: string
|
|
12
|
+
/** Any additional properties (templated, type, etc.) */
|
|
13
|
+
[key: string]: unknown
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A map of action name → link. Only permitted actions are present. */
|
|
17
|
+
export type LinkMap = Record<string, LinkTemplate>
|
|
18
|
+
|
|
19
|
+
// ─── buildLinks config ────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export interface BuildLinksConfig<
|
|
22
|
+
S extends Subject = Subject,
|
|
23
|
+
R extends Resource = Resource,
|
|
24
|
+
> {
|
|
25
|
+
engine: AuthEvaluator<S, R>
|
|
26
|
+
subject: S
|
|
27
|
+
resource?: R
|
|
28
|
+
/**
|
|
29
|
+
* Map of action name → link template.
|
|
30
|
+
* Only entries whose action is permitted will appear in the result.
|
|
31
|
+
*/
|
|
32
|
+
actions: Record<Action, LinkTemplate>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── buildLinks ───────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Evaluates every action in `config.actions` and returns a `LinkMap`
|
|
39
|
+
* containing only the links the subject is permitted to follow.
|
|
40
|
+
*
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const links = await buildLinks({
|
|
43
|
+
* engine,
|
|
44
|
+
* subject,
|
|
45
|
+
* resource,
|
|
46
|
+
* actions: {
|
|
47
|
+
* read: { href: `/documents/${id}`, method: 'GET' },
|
|
48
|
+
* write: { href: `/documents/${id}`, method: 'PUT' },
|
|
49
|
+
* delete: { href: `/documents/${id}`, method: 'DELETE' },
|
|
50
|
+
* archive: { href: `/documents/${id}/archive`, method: 'POST' },
|
|
51
|
+
* },
|
|
52
|
+
* })
|
|
53
|
+
* // → only links whose policy decision was allowed: true
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export async function buildLinks<
|
|
57
|
+
S extends Subject = Subject,
|
|
58
|
+
R extends Resource = Resource,
|
|
59
|
+
>(config: BuildLinksConfig<S, R>): Promise<LinkMap> {
|
|
60
|
+
const actionNames = Object.keys(config.actions)
|
|
61
|
+
|
|
62
|
+
const permitted = config.resource !== undefined
|
|
63
|
+
? await config.engine.permissions(config.subject, config.resource, actionNames)
|
|
64
|
+
: await config.engine.permissions(config.subject, actionNames)
|
|
65
|
+
|
|
66
|
+
const links: LinkMap = {}
|
|
67
|
+
for (const action of actionNames) {
|
|
68
|
+
if (permitted[action]) {
|
|
69
|
+
links[action] = config.actions[action]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return links
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── embedLinks ───────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
export interface EmbedLinksConfig<
|
|
79
|
+
S extends Subject = Subject,
|
|
80
|
+
R extends Resource = Resource,
|
|
81
|
+
> extends BuildLinksConfig<S, R> {
|
|
82
|
+
/**
|
|
83
|
+
* A `self` link added unconditionally — it is not subject to policy
|
|
84
|
+
* evaluation since the resource has already been fetched and returned.
|
|
85
|
+
*/
|
|
86
|
+
self?: LinkTemplate
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Builds permission-aware links and merges them into `data` as a `_links`
|
|
91
|
+
* property, following HAL (application/hal+json) conventions.
|
|
92
|
+
*
|
|
93
|
+
* ```typescript
|
|
94
|
+
* const body = await embedLinks(document, {
|
|
95
|
+
* engine, subject, resource: document,
|
|
96
|
+
* self: { href: `/documents/${document.id}`, method: 'GET' },
|
|
97
|
+
* actions: {
|
|
98
|
+
* write: { href: `/documents/${document.id}`, method: 'PUT' },
|
|
99
|
+
* delete: { href: `/documents/${document.id}`, method: 'DELETE' },
|
|
100
|
+
* },
|
|
101
|
+
* })
|
|
102
|
+
* // → { ...document, _links: { self: {...}, write: {...} } }
|
|
103
|
+
* // (delete absent — not permitted)
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export async function embedLinks<
|
|
107
|
+
T extends object,
|
|
108
|
+
S extends Subject = Subject,
|
|
109
|
+
R extends Resource = Resource,
|
|
110
|
+
>(data: T, config: EmbedLinksConfig<S, R>): Promise<T & { _links: LinkMap }> {
|
|
111
|
+
const permitted = await buildLinks(config)
|
|
112
|
+
|
|
113
|
+
const _links: LinkMap = {}
|
|
114
|
+
if (config.self) _links['self'] = config.self
|
|
115
|
+
Object.assign(_links, permitted)
|
|
116
|
+
|
|
117
|
+
return { ...data, _links }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Decision-based link filtering (sync, from pre-fetched decisions) ─────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Synchronous variant for cases where you have already called `engine.permissions()`
|
|
124
|
+
* and want to build links without an additional async round-trip.
|
|
125
|
+
*
|
|
126
|
+
* ```typescript
|
|
127
|
+
* const perms = await engine.permissions(subject, resource, ['read', 'write', 'delete'])
|
|
128
|
+
* const links = linksFromDecisions(perms, actionTemplates)
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export function linksFromDecisions(
|
|
132
|
+
permissions: Record<string, boolean>,
|
|
133
|
+
actions: Record<Action, LinkTemplate>,
|
|
134
|
+
): LinkMap {
|
|
135
|
+
const links: LinkMap = {}
|
|
136
|
+
for (const [action, template] of Object.entries(actions)) {
|
|
137
|
+
if (permissions[action]) {
|
|
138
|
+
links[action] = template
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return links
|
|
142
|
+
}
|