@occultist/occultist 0.0.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.
Files changed (165) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/dist/accept.d.ts +41 -0
  4. package/dist/accept.js +110 -0
  5. package/dist/accept.test.d.ts +1 -0
  6. package/dist/accept.test.js +44 -0
  7. package/dist/action.test.d.ts +1 -0
  8. package/dist/action.test.js +1 -0
  9. package/dist/actions/actionSets.d.ts +23 -0
  10. package/dist/actions/actionSets.js +49 -0
  11. package/dist/actions/actions.d.ts +163 -0
  12. package/dist/actions/actions.js +436 -0
  13. package/dist/actions/context.d.ts +78 -0
  14. package/dist/actions/context.js +112 -0
  15. package/dist/actions/meta.d.ts +49 -0
  16. package/dist/actions/meta.js +177 -0
  17. package/dist/actions/path.d.ts +21 -0
  18. package/dist/actions/path.js +83 -0
  19. package/dist/actions/path.test.d.ts +1 -0
  20. package/dist/actions/path.test.js +9 -0
  21. package/dist/actions/spec.d.ts +214 -0
  22. package/dist/actions/spec.js +1 -0
  23. package/dist/actions/types.d.ts +112 -0
  24. package/dist/actions/types.js +2 -0
  25. package/dist/actions/writer.d.ts +27 -0
  26. package/dist/actions/writer.js +140 -0
  27. package/dist/actions/writer.test.d.ts +1 -0
  28. package/dist/actions/writer.test.js +42 -0
  29. package/dist/auth/types.d.ts +14 -0
  30. package/dist/auth/types.js +1 -0
  31. package/dist/cache/cache.d.ts +30 -0
  32. package/dist/cache/cache.js +220 -0
  33. package/dist/cache/etag.d.ts +17 -0
  34. package/dist/cache/etag.js +83 -0
  35. package/dist/cache/etag.test.d.ts +1 -0
  36. package/dist/cache/etag.test.js +91 -0
  37. package/dist/cache/memory.d.ts +12 -0
  38. package/dist/cache/memory.js +36 -0
  39. package/dist/cache/types.d.ts +175 -0
  40. package/dist/cache/types.js +4 -0
  41. package/dist/errors.d.ts +11 -0
  42. package/dist/errors.js +54 -0
  43. package/dist/jsonld.d.ts +43 -0
  44. package/dist/jsonld.js +1 -0
  45. package/dist/makeTypeDefs.d.ts +27 -0
  46. package/dist/makeTypeDefs.js +70 -0
  47. package/dist/merge.d.ts +61 -0
  48. package/dist/merge.js +1 -0
  49. package/dist/mod.d.ts +14 -0
  50. package/dist/mod.js +14 -0
  51. package/dist/processAction.d.ts +15 -0
  52. package/dist/processAction.js +512 -0
  53. package/dist/registry.d.ts +88 -0
  54. package/dist/registry.js +314 -0
  55. package/dist/registry.test.d.ts +1 -0
  56. package/dist/registry.test.js +133 -0
  57. package/dist/request.d.ts +29 -0
  58. package/dist/request.js +118 -0
  59. package/dist/scopes.d.ts +35 -0
  60. package/dist/scopes.js +121 -0
  61. package/dist/scopes.test.d.ts +1 -0
  62. package/dist/scopes.test.js +55 -0
  63. package/dist/transformers/fileTransformer.d.ts +1 -0
  64. package/dist/transformers/fileTransformer.js +8 -0
  65. package/dist/types.d.ts +12 -0
  66. package/dist/types.js +1 -0
  67. package/dist/utils/alwaysArray.d.ts +1 -0
  68. package/dist/utils/alwaysArray.js +9 -0
  69. package/dist/utils/contextBuilder.d.ts +9 -0
  70. package/dist/utils/contextBuilder.js +82 -0
  71. package/dist/utils/getActionContext.d.ts +7 -0
  72. package/dist/utils/getActionContext.js +48 -0
  73. package/dist/utils/getInternalName.d.ts +6 -0
  74. package/dist/utils/getInternalName.js +7 -0
  75. package/dist/utils/getParamLocation.d.ts +2 -0
  76. package/dist/utils/getParamLocation.js +6 -0
  77. package/dist/utils/getPropertyValueSpecifications.d.ts +2 -0
  78. package/dist/utils/getPropertyValueSpecifications.js +49 -0
  79. package/dist/utils/getRequestBodyValues.d.ts +11 -0
  80. package/dist/utils/getRequestBodyValues.js +122 -0
  81. package/dist/utils/getRequestIRIValues.d.ts +14 -0
  82. package/dist/utils/getRequestIRIValues.js +133 -0
  83. package/dist/utils/isBodyInit.d.ts +1 -0
  84. package/dist/utils/isBodyInit.js +21 -0
  85. package/dist/utils/isNil.d.ts +1 -0
  86. package/dist/utils/isNil.js +4 -0
  87. package/dist/utils/isObject.d.ts +6 -0
  88. package/dist/utils/isObject.js +6 -0
  89. package/dist/utils/isPopulatedObject.d.ts +5 -0
  90. package/dist/utils/isPopulatedObject.js +8 -0
  91. package/dist/utils/isPopulatedString.d.ts +1 -0
  92. package/dist/utils/isPopulatedString.js +4 -0
  93. package/dist/utils/joinPaths.d.ts +1 -0
  94. package/dist/utils/joinPaths.js +31 -0
  95. package/dist/utils/makeAppendProblemDetails.d.ts +14 -0
  96. package/dist/utils/makeAppendProblemDetails.js +26 -0
  97. package/dist/utils/makeURLPattern.d.ts +5 -0
  98. package/dist/utils/makeURLPattern.js +12 -0
  99. package/dist/utils/normalizeURL.d.ts +4 -0
  100. package/dist/utils/normalizeURL.js +11 -0
  101. package/dist/utils/parseSearchParams.d.ts +3 -0
  102. package/dist/utils/parseSearchParams.js +24 -0
  103. package/dist/utils/preferredMediaTypes.d.ts +42 -0
  104. package/dist/utils/preferredMediaTypes.js +149 -0
  105. package/dist/utils/urlToIRI.d.ts +1 -0
  106. package/dist/utils/urlToIRI.js +8 -0
  107. package/dist/utils/validateSpecValue.d.ts +1 -0
  108. package/dist/utils/validateSpecValue.js +1 -0
  109. package/dist/validators.d.ts +16 -0
  110. package/dist/validators.js +134 -0
  111. package/lib/accept.test.ts +55 -0
  112. package/lib/accept.ts +147 -0
  113. package/lib/action.test.ts +2 -0
  114. package/lib/actions/actionSets.ts +88 -0
  115. package/lib/actions/actions.ts +795 -0
  116. package/lib/actions/context.ts +170 -0
  117. package/lib/actions/meta.ts +251 -0
  118. package/lib/actions/path.test.ts +15 -0
  119. package/lib/actions/path.ts +99 -0
  120. package/lib/actions/spec.ts +545 -0
  121. package/lib/actions/types.ts +146 -0
  122. package/lib/actions/writer.test.ts +57 -0
  123. package/lib/actions/writer.ts +176 -0
  124. package/lib/auth/types.ts +22 -0
  125. package/lib/cache/cache.ts +291 -0
  126. package/lib/cache/etag.test.ts +122 -0
  127. package/lib/cache/etag.ts +106 -0
  128. package/lib/cache/memory.ts +52 -0
  129. package/lib/cache/types.ts +240 -0
  130. package/lib/errors.ts +66 -0
  131. package/lib/jsonld.ts +67 -0
  132. package/lib/makeTypeDefs.ts +138 -0
  133. package/lib/merge.ts +86 -0
  134. package/lib/mod.ts +14 -0
  135. package/lib/processAction.ts +690 -0
  136. package/lib/registry.test.ts +174 -0
  137. package/lib/registry.ts +455 -0
  138. package/lib/request.ts +153 -0
  139. package/lib/scopes.test.ts +70 -0
  140. package/lib/scopes.ts +178 -0
  141. package/lib/transformers/fileTransformer.ts +10 -0
  142. package/lib/types.ts +13 -0
  143. package/lib/utils/alwaysArray.ts +10 -0
  144. package/lib/utils/contextBuilder.ts +111 -0
  145. package/lib/utils/getActionContext.ts +76 -0
  146. package/lib/utils/getInternalName.ts +15 -0
  147. package/lib/utils/getParamLocation.ts +14 -0
  148. package/lib/utils/getPropertyValueSpecifications.ts +76 -0
  149. package/lib/utils/getRequestBodyValues.ts +155 -0
  150. package/lib/utils/getRequestIRIValues.ts +201 -0
  151. package/lib/utils/isBodyInit.ts +22 -0
  152. package/lib/utils/isNil.ts +4 -0
  153. package/lib/utils/isObject.ts +8 -0
  154. package/lib/utils/isPopulatedObject.ts +9 -0
  155. package/lib/utils/isPopulatedString.ts +4 -0
  156. package/lib/utils/joinPaths.ts +36 -0
  157. package/lib/utils/makeAppendProblemDetails.ts +57 -0
  158. package/lib/utils/makeURLPattern.ts +18 -0
  159. package/lib/utils/normalizeURL.ts +15 -0
  160. package/lib/utils/parseSearchParams.ts +36 -0
  161. package/lib/utils/preferredMediaTypes.ts +220 -0
  162. package/lib/utils/urlToIRI.ts +11 -0
  163. package/lib/utils/validateSpecValue.ts +0 -0
  164. package/lib/validators.ts +186 -0
  165. package/package.json +41 -0
@@ -0,0 +1,57 @@
1
+ import {createServer} from 'node:http';
2
+ import assert from 'node:assert/strict';
3
+ import test from 'node:test';
4
+ import {ResponseWriter} from './writer.js';
5
+ import {AddressInfo} from 'node:net';
6
+
7
+
8
+ test('Writer writes hints', () => {
9
+ return new Promise((resolve, reject) => {
10
+ const server = createServer();
11
+
12
+ server.on('request', async (_req, res) => {
13
+ const writer = new ResponseWriter(res);
14
+ await writer.writeEarlyHints({
15
+ link: [
16
+ {
17
+ href: 'https://example.com/main.css',
18
+ as: 'stylesheet',
19
+ preload: true,
20
+ fetchPriority: 'high',
21
+ },
22
+ {
23
+ href: 'https://example.com/main.js',
24
+ as: 'script',
25
+ preload: true,
26
+ fetchPriority: 'low',
27
+ }
28
+ ],
29
+ });
30
+
31
+ writer.writeHead(200);
32
+
33
+ res.end();
34
+ });
35
+
36
+ server.on('error', (err) => {
37
+ reject(err);
38
+ });
39
+
40
+ server.listen(0, '127.0.0.1', async () => {
41
+ const { port, address } = server.address() as AddressInfo;
42
+
43
+ const res = await fetch(`http://${address}:${port}`);
44
+ await res.text();
45
+
46
+ assert(
47
+ res.headers.get('link'),
48
+ `</https://example.com/main.css>; rel=preload; as=stylesheet; fetchpriority=high, `
49
+ + `</https://example.com/main.js>; rel=preload; as=script; fetchpriority=low`,
50
+ );
51
+
52
+ server.close();
53
+
54
+ resolve();
55
+ });
56
+ });
57
+ });
@@ -0,0 +1,176 @@
1
+ import { ServerResponse } from 'node:http';
2
+ import type { HintLink, HintArgs, HintObj } from './types.js';
3
+
4
+
5
+ function isHintLink(hint: HintObj | HintLink): hint is HintLink {
6
+ return hint.href != null;
7
+ }
8
+
9
+ export type ResponseTypes =
10
+ | ServerResponse
11
+ | Response
12
+ ;
13
+
14
+ export type ResponseBody = BodyInit;
15
+
16
+
17
+ export interface HTTPWriter {
18
+ mergeHeaders(headers: HeadersInit): void;
19
+ writeEarlyHints(args: HintArgs): void;
20
+ mergeHeaders(headers: HeadersInit): void;
21
+ writeHead(status: number, headers?: Headers): void;
22
+ writeBody(body: ResponseBody): void;
23
+ response(): ResponseTypes;
24
+ };
25
+
26
+ export class ResponseWriter implements HTTPWriter {
27
+ #res?: ServerResponse;
28
+ #hints?: {
29
+ link: string | string[];
30
+ };
31
+ #status?: number;
32
+ #statusText?: string;
33
+ #headers: Headers = new Headers();
34
+ #body?: ResponseBody;
35
+
36
+ constructor(
37
+ res?: ServerResponse,
38
+ ) {
39
+ this.#res = res;
40
+ }
41
+
42
+ /**
43
+ * Writes early hints to the request.
44
+ *
45
+ * Runtimes which do not support writing early hints will have the
46
+ * headers added to the headers of the main response instead.
47
+ */
48
+ writeEarlyHints(args: HintArgs): Promise<void> {
49
+ const links: string[] = []
50
+
51
+ if (Array.isArray(args)) {
52
+ for (let i = 0; i < args.length; i++) {
53
+ links.push(this.#formatEarlyHint(args[i]));
54
+ }
55
+ } else if (typeof args === 'function') {
56
+ const res = args();
57
+
58
+ if (Array.isArray(res)) {
59
+ for (let i = 0; i < res.length; i++) {
60
+ links.push(this.#formatEarlyHint(res[i]))
61
+ }
62
+ } else if (isHintLink(res)) {
63
+ links.push(this.#formatEarlyHint(res));
64
+ } else if (Array.isArray(res.link)) {
65
+ for (let i = 0; i < res.link.length; i++) {
66
+ links.push(this.#formatEarlyHint(res.link[i]));
67
+ }
68
+ } else {
69
+ links.push(this.#formatEarlyHint(res.link));
70
+ }
71
+ } else if (isHintLink(args)) {
72
+ links.push(this.#formatEarlyHint(args));
73
+ } else if (Array.isArray(args.link)) {
74
+ for (let i = 0; i < args.link.length; i++) {
75
+ links.push(this.#formatEarlyHint(args.link[i]));
76
+ }
77
+ } else {
78
+ links.push(this.#formatEarlyHint(args.link));
79
+ }
80
+
81
+ if (this.#res == null) {
82
+ this.#headers.append('Link', links.join(', '));
83
+ } else {
84
+ return new Promise((resolve) => {
85
+ this.#res.writeEarlyHints({ 'Link': links.join(', ') }, resolve);
86
+ });
87
+ }
88
+ }
89
+
90
+ mergeHeaders(headersInit: HeadersInit): void {
91
+ this.#setHeaders(new Headers(headersInit));
92
+ }
93
+
94
+ writeHead(status: number, headers?: Headers) {
95
+ const res = this.#res;
96
+
97
+ this.#status = status;
98
+
99
+ if (headers != null) {
100
+ this.#setHeaders(headers);
101
+ }
102
+
103
+ if (res instanceof ServerResponse && this.#hints != null) {
104
+ res.writeHead(status, this.#hints);
105
+ } else if (res instanceof ServerResponse) {
106
+ res.writeHead(status);
107
+ }
108
+ }
109
+
110
+ writeBody(body: ResponseBody): void {
111
+ if (this.#res instanceof ServerResponse) {
112
+ this.#res.write(body);
113
+ } else {
114
+ this.#body = body;
115
+ }
116
+ }
117
+
118
+ response(): Response | ServerResponse {
119
+ if (this.#res instanceof ServerResponse) {
120
+ this.#res.end();
121
+
122
+ return this.#res;
123
+ }
124
+
125
+ if (this.#body instanceof Uint8Array) {
126
+ }
127
+
128
+ return new Response(this.#body as BodyInit, {
129
+ status: this.#status,
130
+ statusText: this.#statusText,
131
+ headers: this.#headers,
132
+ });
133
+ }
134
+
135
+ #setHeaders(headers: Headers): void {
136
+ for (const [header, value] of headers.entries()) {
137
+ if (Array.isArray(value)) {
138
+ for (const item of value) {
139
+ this.#headers.append(header, item);
140
+ }
141
+ } else {
142
+ this.#headers.append(header, value);
143
+ }
144
+ }
145
+ }
146
+
147
+ #formatEarlyHint(hint: HintLink): string {
148
+ let link: string = `<${encodeURI(hint.href)}>`;
149
+
150
+ if (hint.preload) {
151
+ link += `; rel=preload`;
152
+ }
153
+
154
+ if (Array.isArray(hint.rel)) {
155
+ link += '; ' + hint.rel.map((rel) => `rel=${rel}`)
156
+ .join('; ') + '';
157
+ } else if (hint.rel != null) {
158
+ link += `; rel=${hint.rel}`;
159
+ }
160
+
161
+ if (hint.as != null) {
162
+ link += `; as=${hint.as}`;
163
+ }
164
+
165
+ if (hint.fetchPriority != null) {
166
+ link += `; fetchpriority=${hint.fetchPriority}`;
167
+ }
168
+
169
+ if (hint.crossOrigin != null) {
170
+ link += `; crossorigin=${hint.crossOrigin}`;
171
+ }
172
+
173
+ return link;
174
+ }
175
+
176
+ }
@@ -0,0 +1,22 @@
1
+
2
+ export type UnauthenticatedAuthContext = {
3
+ authenticated: false;
4
+ authKey: undefined;
5
+ };
6
+
7
+ export type AuthenticatedAuthContext = {
8
+ authenticated: true;
9
+ authKey: string;
10
+ };
11
+
12
+ // deno-lint-ignore no-explicit-any
13
+ export type AuthState = Record<string, any>;
14
+
15
+ export type AuthMiddlewareResponse<
16
+ State extends AuthState = AuthState,
17
+ > = {
18
+ authKey?: string;
19
+ allowPublic?: boolean;
20
+ state: State;
21
+ };
22
+
@@ -0,0 +1,291 @@
1
+ import {createHash} from 'node:crypto';
2
+ import {CacheContext, NextFn, Registry} from '../mod.js';
3
+ import {EtagConditions} from './etag.js';
4
+ import type {CacheBuilder, CacheEntryDescriptor, CacheETagArgs, CacheETagInstanceArgs, CacheHitHandle, CacheHTTPArgs, CacheHTTPInstanceArgs, CacheMeta, CacheMissHandle, CacheStorage, CacheStoreArgs, CacheStoreInstanceArgs, LockedCacheMissHandle, UpstreamCache} from './types.js';
5
+
6
+
7
+ export class Cache implements CacheBuilder {
8
+ #registry: Registry;
9
+ #cacheMeta: CacheMeta;
10
+ #storage: CacheStorage;
11
+ #upstream?: UpstreamCache;
12
+
13
+ constructor(
14
+ registry: Registry,
15
+ cacheMeta: CacheMeta,
16
+ storage: CacheStorage,
17
+ upstream?: UpstreamCache,
18
+ ) {
19
+ this.#registry = registry;
20
+ this.#cacheMeta = cacheMeta;
21
+ this.#storage = storage;
22
+ this.#upstream = upstream;
23
+ }
24
+
25
+ get registry(): Registry {
26
+ return this.#registry;
27
+ }
28
+
29
+ get meta(): CacheMeta {
30
+ return this.#cacheMeta;
31
+ }
32
+
33
+ get storage(): CacheStorage {
34
+ return this.#storage;
35
+ }
36
+
37
+ get upstream(): UpstreamCache {
38
+ return this.#upstream;
39
+ }
40
+
41
+ /**
42
+ * Add HTTP headers to the request.
43
+ */
44
+ http(args?: CacheHTTPArgs): CacheHTTPInstanceArgs {
45
+ return Object.assign(Object.create(null), args, {
46
+ strategy: 'http',
47
+ cache: this,
48
+ });
49
+ }
50
+
51
+ /**
52
+ * Stores an etag value of the response and adds HTTP headers to the request.
53
+ * Requests made to an endpoint implementing etag cache can use `If-None-Match`
54
+ * or `If-Modified-Since` headers to test
55
+ */
56
+ etag(args?: CacheETagArgs): CacheETagInstanceArgs {
57
+ return Object.assign(Object.create(null), args, {
58
+ strategy: 'etag',
59
+ cache: this,
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Caches the body of the response, stores and etag and adds HTTP headers to the request.
65
+ */
66
+ store(args?: CacheStoreArgs): CacheStoreInstanceArgs {
67
+ return Object.assign(Object.create(null), args, {
68
+ strategy: 'store',
69
+ cache: this,
70
+ });
71
+ }
72
+
73
+ async push(_req: Request): Promise<void> {
74
+
75
+ }
76
+
77
+ async invalidate(_req: Request): Promise<void> {
78
+
79
+ }
80
+ }
81
+
82
+ export class CacheMiddleware {
83
+ async use(
84
+ descriptors: CacheEntryDescriptor[],
85
+ ctx: CacheContext,
86
+ next: NextFn,
87
+ ): Promise<void> {
88
+ const descriptor = descriptors.find((descriptor) => {
89
+ const when = descriptor.args.when;
90
+
91
+ if (when == null) {
92
+ return true;
93
+ } else if (when === 'always') {
94
+ return true;
95
+ } else if (when === 'public' && ctx.authKey == null) {
96
+ return true;
97
+ } else if (when === 'private' && ctx.authKey != null) {
98
+ return true;
99
+ } else if (typeof when === 'function') {
100
+ return when(ctx);
101
+ }
102
+
103
+ return false;
104
+ });
105
+
106
+ if (descriptor == null) {
107
+ return await next();
108
+ }
109
+
110
+ switch (descriptor.args.strategy) {
111
+ case 'http': {
112
+ await this.#useHTTP(descriptor, ctx, next);
113
+ break;
114
+ }
115
+ case 'etag': {
116
+ await this.#useEtag(descriptor, ctx, next);
117
+ break;
118
+ }
119
+ case 'store': {
120
+ await this.#useStore(descriptor, ctx, next);
121
+ break;
122
+ }
123
+ }
124
+ }
125
+
126
+ /**
127
+ * @todo Implement vary rules.
128
+ */
129
+ #makeKey(descriptor: CacheEntryDescriptor): string {
130
+ const { contentType } = descriptor;
131
+ const { version } = descriptor.args;
132
+ const { name } = descriptor.action;
133
+ const { url } = descriptor.request;
134
+
135
+ return 'v' + (version ?? 0) + '|' + name + '|' + contentType.toLowerCase() + '|' + url.toString();
136
+ }
137
+
138
+ #setHeaders(
139
+ descriptor: CacheEntryDescriptor,
140
+ ctx: CacheContext,
141
+ ): void {
142
+ const {
143
+
144
+ } = descriptor.args;
145
+ }
146
+
147
+ async #useHTTP(
148
+ descriptor: CacheEntryDescriptor,
149
+ ctx: CacheContext,
150
+ next: NextFn,
151
+ ): Promise<void> {
152
+ this.#setHeaders(descriptor, ctx);
153
+
154
+ await next();
155
+ }
156
+
157
+ async #useEtag(
158
+ descriptor: CacheEntryDescriptor,
159
+ ctx: CacheContext,
160
+ next: NextFn,
161
+ ): Promise<void> {
162
+ const key = this.#makeKey(descriptor);
163
+ const rules = new EtagConditions(ctx.req.headers);
164
+ const resourceState = await descriptor.args.cache.meta.get(key);
165
+
166
+ if (resourceState.type === 'cache-hit') {
167
+ if (rules.ifMatch(resourceState.etag)) {
168
+ return;
169
+ } else if (!rules.ifNoneMatch(resourceState.etag)) {
170
+ ctx.hit = true;
171
+ ctx.status = 304;
172
+
173
+ return
174
+ }
175
+ }
176
+
177
+ this.#setHeaders(descriptor, ctx);
178
+
179
+ await next();
180
+ }
181
+
182
+ async #useStore(
183
+ descriptor: CacheEntryDescriptor,
184
+ ctx: CacheContext,
185
+ next: NextFn,
186
+ ): Promise<void> {
187
+ const key = this.#makeKey(descriptor);
188
+ let resourceState: CacheHitHandle | CacheMissHandle | LockedCacheMissHandle | undefined;
189
+
190
+ if (
191
+ typeof descriptor.args.cache.meta.getOrLock === 'function' &&
192
+ descriptor.args.lock
193
+ ) {
194
+ try {
195
+ resourceState = await descriptor.args.cache.meta.getOrLock(key);
196
+ } catch (err) {
197
+ resourceState = await descriptor.args.cache.meta.get(key);
198
+ }
199
+ } else {
200
+ resourceState = await descriptor.args.cache.meta.get(key);
201
+ }
202
+
203
+ if (resourceState?.type === 'cache-hit') {
204
+ if (this.#isNotModified(ctx.req.headers, resourceState.etag)) {
205
+ ctx.hit = true;
206
+ ctx.status = 304;
207
+
208
+ return;
209
+ }
210
+
211
+ if (resourceState.hasContent) {
212
+ try {
213
+ ctx.hit = true;
214
+ ctx.status = resourceState.status;
215
+ ctx.body = await descriptor.args.cache.storage.get(key);
216
+
217
+ for (const [key, value] of resourceState.headers.entries()) {
218
+ ctx.headers.set(key, value);
219
+ }
220
+
221
+ return;
222
+ } catch (err) {
223
+ console.log(err);
224
+ }
225
+ }
226
+ }
227
+
228
+ try {
229
+ this.#setHeaders(descriptor, ctx);
230
+
231
+ await next();
232
+
233
+ const body = await new Response(ctx.body).blob();
234
+ const etag = await this.#createEtag(body);
235
+
236
+ ctx.etag = etag;
237
+ ctx.headers.set('Etag', etag);
238
+
239
+ await descriptor.args.cache.meta.set(key, {
240
+ key,
241
+ authKey: ctx.authKey,
242
+ iri: ctx.url,
243
+ status: ctx.status ?? 200,
244
+ hasContent: ctx.body != null,
245
+ headers: ctx.headers,
246
+ contentType: ctx.contentType,
247
+ etag,
248
+ });
249
+
250
+ if (ctx.body != null) {
251
+ await descriptor.args.cache.storage.set(key, body);
252
+ }
253
+
254
+ if (resourceState.type === 'locked-cache-miss') {
255
+ await resourceState.release();
256
+ }
257
+
258
+ if (this.#isNotModified(ctx.req.headers, etag)) {
259
+ ctx.hit = true;
260
+ ctx.status = 304;
261
+ ctx.body = null;
262
+ ctx.headers.delete('Etag');
263
+
264
+ return;
265
+ }
266
+ } catch (err) {
267
+ if (resourceState.type === 'locked-cache-miss') {
268
+ await resourceState.release();
269
+ }
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Returns true if the request has conditional headers
275
+ * which should result in a not modified response.
276
+ */
277
+ #isNotModified(headers: Headers, etag: string): boolean {
278
+ const rules = new EtagConditions(headers);
279
+
280
+ return rules.isNotModified(etag);
281
+ }
282
+
283
+ async #createEtag(body: Blob, weak: boolean = true): Promise<string> {
284
+ const buff = await body.bytes();
285
+ const hash = createHash('sha1').update(buff).digest('hex');
286
+ const quoted = `"${hash}"`;
287
+
288
+ return weak ? `W/${quoted}` : quoted;
289
+ }
290
+
291
+ }
@@ -0,0 +1,122 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import {EtagConditions} from './etag.js';
4
+
5
+
6
+ test.describe('conditions.ifMatch()', () => {
7
+ test('returns true when the header has the value of "*" and an etag is present', () => {
8
+ const conditions = new EtagConditions(new Headers({
9
+ 'If-Match': '*',
10
+ }));
11
+
12
+ assert(conditions.ifMatch('"xxxx"'));
13
+ });
14
+
15
+ test('returns false when the header has the value of "*" and an etag is not present', () => {
16
+ const conditions = new EtagConditions(new Headers({
17
+ 'If-Match': '*',
18
+ }));
19
+
20
+ assert(!conditions.ifMatch());
21
+ });
22
+
23
+ test('returns false when the header provides an etag and an etag is not present', () => {
24
+ const conditions = new EtagConditions(new Headers({
25
+ 'If-Match': '"xxxx"',
26
+ }));
27
+
28
+ assert(!conditions.ifMatch());
29
+ });
30
+
31
+ test('returns false when the header provides an etag and a different etag is present', () => {
32
+ const conditions = new EtagConditions(new Headers({
33
+ 'If-Match': '"xxxx"',
34
+ }));
35
+
36
+ assert(!conditions.ifMatch('"yyyy"'));
37
+ });
38
+
39
+ test('returns false when the header provides a matching but weak etag', () => {
40
+ const conditions = new EtagConditions(new Headers({
41
+ 'If-Match': 'W\/"xxxx"',
42
+ }));
43
+
44
+ assert(!conditions.ifMatch('W\/"xxxx"'));
45
+ });
46
+
47
+ test('returns true when the header provides a matching etag', () => {
48
+ const conditions = new EtagConditions(new Headers({
49
+ 'If-Match': '"xxxx"',
50
+ }));
51
+
52
+ assert(conditions.ifMatch('"xxxx"'));
53
+ });
54
+
55
+ test('returns true when the header provides a list containing a matching etag', () => {
56
+ const conditions = new EtagConditions(new Headers({
57
+ 'If-Match': '"xxxx", "yyyy", "zzzz"',
58
+ }));
59
+
60
+ assert(conditions.ifMatch('"yyyy"'));
61
+ });
62
+ });
63
+
64
+
65
+ test.describe('conditions.ifNoneMatch()', () => {
66
+ test('returns true when the header has the value of "*" and an etag is not present', () => {
67
+ const conditions = new EtagConditions(new Headers({
68
+ 'If-None-Match': '*',
69
+ }));
70
+
71
+ assert(conditions.ifNoneMatch());
72
+ });
73
+
74
+ test('returns false when the header has the value of "*" and an etag is present', () => {
75
+ const conditions = new EtagConditions(new Headers({
76
+ 'If-None-Match': '*',
77
+ }));
78
+
79
+ assert(!conditions.ifNoneMatch('W\/"xxxx"'));
80
+ });
81
+
82
+ test('returns true when the header provides an etag and no representation etag is present', () => {
83
+ const conditions = new EtagConditions(new Headers({
84
+ 'If-None-Match': 'W\/"xxxx"',
85
+ }));
86
+
87
+ assert(conditions.ifNoneMatch());
88
+ });
89
+
90
+ test('returns true when the header provides a non-matching etag', () => {
91
+ const conditions = new EtagConditions(new Headers({
92
+ 'If-None-Match': 'W\/"xxxx"',
93
+ }));
94
+
95
+ assert(conditions.ifNoneMatch('W\/"yyyy"'));
96
+ });
97
+
98
+ test('returns true when the header provides a non-matching but strong etag', () => {
99
+ const conditions = new EtagConditions(new Headers({
100
+ 'If-None-Match': '"yyyy"',
101
+ }));
102
+
103
+ assert(conditions.ifNoneMatch('"xxxx"'));
104
+ });
105
+
106
+ test('returns false when the header provides a matching etag', () => {
107
+ const conditions = new EtagConditions(new Headers({
108
+ 'If-None-Match': 'W\/"xxxx"',
109
+ }));
110
+
111
+ assert(!conditions.ifNoneMatch('W\/"xxxx"'));
112
+ });
113
+
114
+ test('returns false when the header provides a list containing a matching etag', () => {
115
+ const conditions = new EtagConditions(new Headers({
116
+ 'If-None-Match': 'W\/"xxxx", W\/"yyyy", W\/"zzzz"',
117
+ }));
118
+
119
+ assert(!conditions.ifNoneMatch('W\/"yyyy"'));
120
+ });
121
+ });
122
+