tshex-cli 1.0.30 → 1.0.31
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/docs/generated-file-reference.md +3 -11
- package/docs/library-structure.md +2 -1
- package/docs/shared/application/data.md +2 -1
- package/docs/shared/domain/entities.md +9 -7
- package/package.json +1 -1
- package/readme.md +2 -9
- package/docs/shared/application/http/handlers.md +0 -102
- package/docs/shared/application/http/json-api.md +0 -235
- package/docs/shared/application/http/json-web-token.md +0 -209
- package/docs/shared/application/http/opengraph.md +0 -161
|
@@ -63,21 +63,13 @@ contracts that adapters and services can share.
|
|
|
63
63
|
#### Shared HTTP Files
|
|
64
64
|
|
|
65
65
|
The `shared/application/http` directory groups the framework-agnostic HTTP
|
|
66
|
-
boundary
|
|
66
|
+
boundary contracts.
|
|
67
67
|
|
|
68
68
|
| File | Responsibility |
|
|
69
69
|
| --- | --- |
|
|
70
|
-
| `shared/application/http/handlers.ts` | Declares `HttpRequestHandler` and `HttpMiddleware`. |
|
|
71
70
|
| `shared/application/http/errors.ts` | Declares `HttpError`. |
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
| `shared/application/http/opengraph.ts` | Type-only Open Graph, Twitter Card, and social metadata declarations. |
|
|
75
|
-
|
|
76
|
-
`handlers.ts` and `errors.ts` are the only files in this directory with
|
|
77
|
-
runtime code. `json-api.ts`, `json-web-token.ts`, and `opengraph.ts` contain
|
|
78
|
-
compile-time structure only; they describe the shape of external formats
|
|
79
|
-
without implementing parsing, validation, or serialization. Each file is
|
|
80
|
-
documented in its own page under `shared/application/http/*.md`.
|
|
71
|
+
|
|
72
|
+
`errors.ts` is documented in its own page under `shared/application/http/errors.md`.
|
|
81
73
|
|
|
82
74
|
#### Shared Data Files
|
|
83
75
|
|
|
@@ -193,9 +193,10 @@ what exists beyond that port depends on the system that implements it.
|
|
|
193
193
|
import { Example } from './enrollment/example-ports.ts'
|
|
194
194
|
import { Student } from './enrollment/domain/students.ts'
|
|
195
195
|
import { Course } from './enrollment/domain/courses.ts'
|
|
196
|
+
import { Email } from './shared/domain/value-objects.ts'
|
|
196
197
|
|
|
197
198
|
const example = new Example()
|
|
198
|
-
const student = new Student('Ada Lovelace', 'ada@example.com')
|
|
199
|
+
const student = new Student('Ada Lovelace', Email.from('ada@example.com'))
|
|
199
200
|
const course = new Course('Mathematics', 'Fundamentals of algebra and calculus', 40)
|
|
200
201
|
|
|
201
202
|
example.createStudent(student)
|
|
@@ -193,6 +193,7 @@ import type { CourseData, StudentData, InscriptionData } from './managers.ts'
|
|
|
193
193
|
import { Course } from '../domain/courses.ts'
|
|
194
194
|
import { Student } from '../domain/students.ts'
|
|
195
195
|
import { Inscription } from '../domain/inscriptions.ts'
|
|
196
|
+
import { Email } from '../shared/domain/value-objects.ts'
|
|
196
197
|
|
|
197
198
|
export class CoursesRepository extends Repository<CourseData, Course, InMemoryDatabaseManager> {
|
|
198
199
|
[property: string]: unknown
|
|
@@ -222,7 +223,7 @@ export class StudentsRepository extends Repository<StudentData, Student, InMemor
|
|
|
222
223
|
}
|
|
223
224
|
|
|
224
225
|
protected transform(data: StudentData): Student {
|
|
225
|
-
return new Student(data.name, data.email)
|
|
226
|
+
return new Student(data.name, Email.from(data.email))
|
|
226
227
|
}
|
|
227
228
|
|
|
228
229
|
public create(student: Student): boolean {
|
|
@@ -35,25 +35,26 @@ In the following example we model the entities of a course enrollment context.
|
|
|
35
35
|
|
|
36
36
|
```ts title="enrollment/domain/students.ts"
|
|
37
37
|
import { Entity } from '../shared/domain/entities.ts'
|
|
38
|
+
import { Email } from '../shared/domain/value-objects.ts'
|
|
38
39
|
|
|
39
40
|
export class Student extends Entity {
|
|
40
41
|
[property: string]: unknown
|
|
41
42
|
|
|
42
43
|
constructor(
|
|
43
44
|
public name: string,
|
|
44
|
-
public email:
|
|
45
|
+
public email: Email
|
|
45
46
|
) {
|
|
46
47
|
super()
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
public equals(other: Student): boolean {
|
|
50
|
-
return this.email
|
|
51
|
+
return this.email.equals(other.email)
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
public override toJSON() {
|
|
54
55
|
return {
|
|
55
56
|
name: this.name,
|
|
56
|
-
email: this.email
|
|
57
|
+
email: this.email.value
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
}
|
|
@@ -121,10 +122,11 @@ export class Inscription extends Entity {
|
|
|
121
122
|
|
|
122
123
|
The most important design decision in an entity is the identity comparison.
|
|
123
124
|
|
|
124
|
-
`Student` uses `
|
|
125
|
-
represent the same person. `
|
|
126
|
-
|
|
127
|
-
student and
|
|
125
|
+
`Student` uses `Email` as the identity because two students with the same
|
|
126
|
+
address represent the same person. `equals()` delegates to `Email.equals()` so
|
|
127
|
+
the comparison rule lives in the value object. `Course` uses `name`.
|
|
128
|
+
`Inscription` combines the student and course identities — two inscriptions are
|
|
129
|
+
the same when both the student and the course match.
|
|
128
130
|
|
|
129
131
|
> **Hint**
|
|
130
132
|
> Keep `equals()` explicit and small. If the comparison starts depending on many
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -86,16 +86,13 @@ core/
|
|
|
86
86
|
|-- shared/
|
|
87
87
|
| |-- application/
|
|
88
88
|
| | |-- data/
|
|
89
|
+
| | | |-- capabilities.ts
|
|
89
90
|
| | | |-- drivers.ts
|
|
90
91
|
| | | |-- managers.ts
|
|
91
92
|
| | | `-- repositories.ts
|
|
92
93
|
| | |-- events.ts
|
|
93
94
|
| | |-- http/
|
|
94
|
-
| | |
|
|
95
|
-
| | | |-- handlers.ts
|
|
96
|
-
| | | |-- json-api.ts
|
|
97
|
-
| | | |-- json-web-token.ts
|
|
98
|
-
| | | `-- opengraph.ts
|
|
95
|
+
| | | `-- errors.ts
|
|
99
96
|
| | |-- loggers.ts
|
|
100
97
|
| | |-- services.ts
|
|
101
98
|
| | `-- validations.ts
|
|
@@ -248,10 +245,6 @@ From this point on, the guide is split into dedicated documents under `docs/`.
|
|
|
248
245
|
- [shared/application/data](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/data.md)
|
|
249
246
|
- [shared/application/events.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/events.md)
|
|
250
247
|
- [shared/application/http/errors.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/http/errors.md)
|
|
251
|
-
- [shared/application/http/handlers.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/http/handlers.md)
|
|
252
|
-
- [shared/application/http/json-api.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/http/json-api.md)
|
|
253
|
-
- [shared/application/http/json-web-token.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/http/json-web-token.md)
|
|
254
|
-
- [shared/application/http/opengraph.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/http/opengraph.md)
|
|
255
248
|
- [shared/application/loggers.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/loggers.md)
|
|
256
249
|
- [shared/application/services.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/services.md)
|
|
257
250
|
- [shared/application/validations.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/validations.md)
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
### HTTP Handlers
|
|
2
|
-
|
|
3
|
-
The handler contracts define a framework-agnostic, transport-facing boundary.
|
|
4
|
-
They are used when an adapter needs to describe request processing and
|
|
5
|
-
cross-cutting request logic in a consistent way, without depending on a
|
|
6
|
-
specific server framework.
|
|
7
|
-
|
|
8
|
-
The generated template relies on the standard `Request` and `Response` types
|
|
9
|
-
from the Fetch API, so adapters work directly with the platform's own APIs.
|
|
10
|
-
|
|
11
|
-
#### Request Handler
|
|
12
|
-
|
|
13
|
-
`HttpRequestHandler` is responsible for processing a request and returning a
|
|
14
|
-
response.
|
|
15
|
-
|
|
16
|
-
```ts title="shared/application/http/handlers.ts"
|
|
17
|
-
export interface HttpRequestHandler {
|
|
18
|
-
handle(request: Request): Response | Promise<Response>
|
|
19
|
-
}
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
In the following example we implement a handler using the standard `Request`
|
|
23
|
-
and `Response` objects.
|
|
24
|
-
|
|
25
|
-
```ts title="users/adapters/get-user-handler.ts"
|
|
26
|
-
import { HttpRequestHandler } from '../../shared/application/http/handlers.js'
|
|
27
|
-
|
|
28
|
-
export class GetUserHandler implements HttpRequestHandler {
|
|
29
|
-
public handle(request: Request): Response {
|
|
30
|
-
const id = new URL(request.url).pathname.split('/').at(-1)
|
|
31
|
-
|
|
32
|
-
return Response.json({ data: { id } }, { status: 200 })
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Using the standard `Request` and `Response` types means the adapter relies on
|
|
38
|
-
the platform's own APIs, such as `request.url`, `request.headers`, and
|
|
39
|
-
`Response.json`. The shape of the JSON body itself, when the adapter follows
|
|
40
|
-
JSON:API, is described by the document types in
|
|
41
|
-
`shared/application/http/json-api.md`.
|
|
42
|
-
|
|
43
|
-
`handle()` can be synchronous or asynchronous; both `Response` and
|
|
44
|
-
`Promise<Response>` are valid return types, so an adapter that awaits a
|
|
45
|
-
database call satisfies the same contract as one that returns immediately.
|
|
46
|
-
|
|
47
|
-
#### Middleware
|
|
48
|
-
|
|
49
|
-
`HttpMiddleware` is responsible for running logic before or around the
|
|
50
|
-
handler.
|
|
51
|
-
|
|
52
|
-
```ts title="shared/application/http/handlers.ts"
|
|
53
|
-
export interface HttpMiddleware {
|
|
54
|
-
process(
|
|
55
|
-
request: Request,
|
|
56
|
-
handler: HttpRequestHandler,
|
|
57
|
-
): Response | Promise<Response>
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Now that the handler exists, middleware can wrap it.
|
|
62
|
-
|
|
63
|
-
```ts title="users/adapters/request-logger.ts"
|
|
64
|
-
import {
|
|
65
|
-
HttpMiddleware,
|
|
66
|
-
HttpRequestHandler,
|
|
67
|
-
} from '../../shared/application/http/handlers.js'
|
|
68
|
-
|
|
69
|
-
export class RequestLoggerMiddleware implements HttpMiddleware {
|
|
70
|
-
public async process(
|
|
71
|
-
request: Request,
|
|
72
|
-
handler: HttpRequestHandler,
|
|
73
|
-
): Promise<Response> {
|
|
74
|
-
void request
|
|
75
|
-
return handler.handle(request)
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
This middleware passes the request through unchanged, showing where
|
|
81
|
-
cross-cutting behavior belongs in the generated HTTP abstraction. A
|
|
82
|
-
short-circuiting middleware, such as an authentication guard, follows the same
|
|
83
|
-
shape but returns a `Response` (for example built from
|
|
84
|
-
`shared/application/http/errors.md`'s `HttpError`) without calling
|
|
85
|
-
`handler.handle()`.
|
|
86
|
-
|
|
87
|
-
> **Note**
|
|
88
|
-
> The handler contracts define the minimum boundary for adapters. Routing,
|
|
89
|
-
> middleware chaining/composition, and status code policies beyond
|
|
90
|
-
> `HttpError` are the responsibility of the concrete transport.
|
|
91
|
-
|
|
92
|
-
#### Example Flow
|
|
93
|
-
|
|
94
|
-
```mermaid
|
|
95
|
-
flowchart LR
|
|
96
|
-
request[Request] --> middleware[Middleware]
|
|
97
|
-
middleware --> handler[Handler]
|
|
98
|
-
handler --> response["Response body"]
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
This flow keeps the transport boundary explicit while leaving framework
|
|
102
|
-
choices to the adapter layer.
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
### JSON:API
|
|
2
|
-
|
|
3
|
-
`json-api.ts` declares a type-only implementation of the
|
|
4
|
-
[JSON:API v1.1](https://jsonapi.org/format/) specification, including the
|
|
5
|
-
[Atomic Operations extension](https://jsonapi.org/ext/atomic/). It gives an
|
|
6
|
-
adapter a shared vocabulary for request and response bodies without forcing a
|
|
7
|
-
particular server framework.
|
|
8
|
-
|
|
9
|
-
This module has no runtime code. It only describes compile-time structure;
|
|
10
|
-
rules that depend on runtime values, URI validity, document-wide uniqueness,
|
|
11
|
-
full linkage, HTTP semantics, or member-name character validation still
|
|
12
|
-
require explicit checks in the adapter.
|
|
13
|
-
|
|
14
|
-
#### Resource Identifiers
|
|
15
|
-
|
|
16
|
-
A resource is referenced in one of two ways, depending on whether the server
|
|
17
|
-
has assigned it a permanent id yet.
|
|
18
|
-
|
|
19
|
-
| Type | Option | Required members |
|
|
20
|
-
| --- | --- | --- |
|
|
21
|
-
| `JsonApiPersistedResourceIdentifier` | Server-assigned id | `type`, `id` |
|
|
22
|
-
| `JsonApiLocalResourceIdentifier` | Client-assigned local id, not yet persisted | `type`, `lid` |
|
|
23
|
-
|
|
24
|
-
`JsonApiResourceIdentifier` is the union of both.
|
|
25
|
-
|
|
26
|
-
```ts
|
|
27
|
-
import type { JsonApiResourceIdentifier } from '../../../shared/application/http/json-api.js'
|
|
28
|
-
|
|
29
|
-
const persisted: JsonApiResourceIdentifier<'users'> = { type: 'users', id: '1' }
|
|
30
|
-
const local: JsonApiResourceIdentifier<'users'> = { type: 'users', lid: 'tmp-1' }
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Resource linkage (`JsonApiResourceLinkage`) is either a single identifier or
|
|
34
|
-
`null` (`JsonApiToOneLinkage`), or an array of identifiers
|
|
35
|
-
(`JsonApiToManyLinkage`).
|
|
36
|
-
|
|
37
|
-
#### Resource Objects
|
|
38
|
-
|
|
39
|
-
A resource object has the same persisted-versus-local split as its
|
|
40
|
-
identifier.
|
|
41
|
-
|
|
42
|
-
| Type | Option | `id` | `lid` |
|
|
43
|
-
| --- | --- | --- | --- |
|
|
44
|
-
| `JsonApiResourceObject` | Server-originated resource | required | optional |
|
|
45
|
-
| `JsonApiNewResourceObject` | Client-originated resource without a server id | never | optional |
|
|
46
|
-
|
|
47
|
-
`JsonApiCreateResourceObject` is the union accepted by create requests, since
|
|
48
|
-
a client is allowed to submit a client-generated id.
|
|
49
|
-
|
|
50
|
-
```ts title="users/adapters/get-user-handler.ts"
|
|
51
|
-
import { HttpRequestHandler } from '../../shared/application/http/handlers.js'
|
|
52
|
-
import {
|
|
53
|
-
JsonApiSingleResourceDocument,
|
|
54
|
-
JsonApiResourceObject,
|
|
55
|
-
} from '../../shared/application/http/json-api.js'
|
|
56
|
-
|
|
57
|
-
type UserAttributes = { email: string }
|
|
58
|
-
type UserResource = JsonApiResourceObject<'users', UserAttributes>
|
|
59
|
-
|
|
60
|
-
export class GetUserHandler implements HttpRequestHandler {
|
|
61
|
-
public handle(request: Request): Response {
|
|
62
|
-
const id = new URL(request.url).pathname.split('/').at(-1) ?? ''
|
|
63
|
-
|
|
64
|
-
const body: JsonApiSingleResourceDocument<UserResource> = {
|
|
65
|
-
data: {
|
|
66
|
-
type: 'users',
|
|
67
|
-
id,
|
|
68
|
-
attributes: { email: 'ada@example.com' },
|
|
69
|
-
},
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return Response.json(body, { status: 200 })
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
#### Relationships
|
|
78
|
-
|
|
79
|
-
`JsonApiRelationship` requires at least one of `links`, `data`, or `meta`, per
|
|
80
|
-
the base specification. Two narrower aliases fix the shape of `data`:
|
|
81
|
-
|
|
82
|
-
| Type | `data` shape |
|
|
83
|
-
| --- | --- |
|
|
84
|
-
| `JsonApiToOneRelationship` | single identifier or `null` |
|
|
85
|
-
| `JsonApiToManyRelationship` | array of identifiers |
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
import type {
|
|
89
|
-
JsonApiToOneRelationship,
|
|
90
|
-
JsonApiToManyRelationship,
|
|
91
|
-
} from '../../../shared/application/http/json-api.js'
|
|
92
|
-
|
|
93
|
-
const author: JsonApiToOneRelationship<{ type: 'authors' }> = {
|
|
94
|
-
data: { type: 'authors', id: '9' },
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const comments: JsonApiToManyRelationship<{ type: 'comments' }> = {
|
|
98
|
-
data: [{ type: 'comments', id: '1' }, { type: 'comments', id: '2' }],
|
|
99
|
-
}
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
#### Documents
|
|
103
|
-
|
|
104
|
-
`JsonApiDocument` is a discriminated union over which top-level member is
|
|
105
|
-
required. Exactly one of these three shapes is valid at a time:
|
|
106
|
-
|
|
107
|
-
| Type | Required top-level member | Use |
|
|
108
|
-
| --- | --- | --- |
|
|
109
|
-
| `JsonApiDataDocument` | `data` | success responses returning resources |
|
|
110
|
-
| `JsonApiErrorDocument` | `errors` (non-empty) | failure responses |
|
|
111
|
-
| `JsonApiMetaDocument` | `meta` | responses with no primary data, such as a heartbeat |
|
|
112
|
-
|
|
113
|
-
`JsonApiDataDocument` itself is narrowed by four ready-made aliases depending
|
|
114
|
-
on what `data` contains:
|
|
115
|
-
|
|
116
|
-
| Alias | `data` shape |
|
|
117
|
-
| --- | --- |
|
|
118
|
-
| `JsonApiSingleResourceDocument` | resource or `null` |
|
|
119
|
-
| `JsonApiResourceCollectionDocument` | array of resources |
|
|
120
|
-
| `JsonApiSingleIdentifierDocument` | identifier or `null` |
|
|
121
|
-
| `JsonApiIdentifierCollectionDocument` | array of identifiers |
|
|
122
|
-
| `JsonApiRelationshipDocument` | any resource linkage |
|
|
123
|
-
|
|
124
|
-
```ts
|
|
125
|
-
import type {
|
|
126
|
-
JsonApiErrorDocument,
|
|
127
|
-
JsonApiMetaDocument,
|
|
128
|
-
} from '../../../shared/application/http/json-api.js'
|
|
129
|
-
|
|
130
|
-
const notFound: JsonApiErrorDocument = {
|
|
131
|
-
errors: [{ status: '404', title: 'Not Found' }],
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const heartbeat: JsonApiMetaDocument<{ status: string }> = {
|
|
135
|
-
meta: { status: 'ok' },
|
|
136
|
-
}
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
#### Request Documents
|
|
140
|
-
|
|
141
|
-
Three request-only document shapes fix `data` to exactly what each operation
|
|
142
|
-
accepts:
|
|
143
|
-
|
|
144
|
-
| Type | `data` shape | Use |
|
|
145
|
-
| --- | --- | --- |
|
|
146
|
-
| `JsonApiCreateDocument` | `JsonApiCreateResourceObject` | `POST` requests |
|
|
147
|
-
| `JsonApiUpdateDocument` | `JsonApiResourceObject` | `PATCH` requests |
|
|
148
|
-
| `JsonApiRelationshipUpdateDocument` | resource linkage | relationship endpoints |
|
|
149
|
-
|
|
150
|
-
`JsonApiQueryParameters` describes the standard `include`, `sort`, `filter`,
|
|
151
|
-
and `page` query members, plus the `fields[TYPE]`, `filter[FIELD]`, and
|
|
152
|
-
`page[FIELD]` bracketed member-name patterns.
|
|
153
|
-
|
|
154
|
-
#### Errors
|
|
155
|
-
|
|
156
|
-
`JsonApiError` requires at least one of `id`, `links`, `status`, `code`,
|
|
157
|
-
`title`, `detail`, `source`, or `meta`.
|
|
158
|
-
|
|
159
|
-
```ts
|
|
160
|
-
import type { JsonApiError } from '../../../shared/application/http/json-api.js'
|
|
161
|
-
|
|
162
|
-
const validationError: JsonApiError = {
|
|
163
|
-
status: '422',
|
|
164
|
-
title: 'Invalid Attribute',
|
|
165
|
-
detail: 'email must be a valid address',
|
|
166
|
-
source: { pointer: '/data/attributes/email' },
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
#### Atomic Operations Extension
|
|
171
|
-
|
|
172
|
-
The Atomic Operations extension adds a request document that carries one or
|
|
173
|
-
more operations, and a response document that carries their results. Every
|
|
174
|
-
operation targets either a resource or a relationship, and every operation
|
|
175
|
-
uses exactly one of three op codes. `JsonApiAtomicStrictOperation` is the
|
|
176
|
-
union of the seven concrete shapes that combination produces:
|
|
177
|
-
|
|
178
|
-
| Type | `op` | Target | `data` |
|
|
179
|
-
| --- | --- | --- | --- |
|
|
180
|
-
| `JsonApiAtomicAddResourceOperation` | `'add'` | resource (`ref` optional, `href` optional) | resource to create |
|
|
181
|
-
| `JsonApiAtomicUpdateResourceOperation` | `'update'` | resource | resource to replace |
|
|
182
|
-
| `JsonApiAtomicRemoveResourceOperation` | `'remove'` | resource (required) | none |
|
|
183
|
-
| `JsonApiAtomicUpdateToOneRelationshipOperation` | `'update'` | relationship (required) | identifier or `null` |
|
|
184
|
-
| `JsonApiAtomicAddToManyRelationshipOperation` | `'add'` | relationship (required) | identifier array |
|
|
185
|
-
| `JsonApiAtomicUpdateToManyRelationshipOperation` | `'update'` | relationship (required) | identifier array |
|
|
186
|
-
| `JsonApiAtomicRemoveFromManyRelationshipOperation` | `'remove'` | relationship (required) | identifier array |
|
|
187
|
-
|
|
188
|
-
A target is expressed through `ref` (a `JsonApiAtomicResourceRef` or
|
|
189
|
-
`JsonApiAtomicRelationshipRef`) or through `href`, never both at once
|
|
190
|
-
(`JsonApiExclusive`).
|
|
191
|
-
|
|
192
|
-
```ts title="users/adapters/atomic-operations-handler.ts"
|
|
193
|
-
import {
|
|
194
|
-
JsonApiAtomicOperationsDocument,
|
|
195
|
-
JsonApiAtomicResultsDocument,
|
|
196
|
-
JsonApiAtomicStrictOperation,
|
|
197
|
-
} from '../../shared/application/http/json-api.js'
|
|
198
|
-
|
|
199
|
-
const request: JsonApiAtomicOperationsDocument<JsonApiAtomicStrictOperation> = {
|
|
200
|
-
'atomic:operations': [
|
|
201
|
-
{ op: 'add', href: '/users', data: { type: 'users', attributes: { email: 'ada@example.com' } } },
|
|
202
|
-
{ op: 'update', ref: { type: 'users', id: '1' }, data: { type: 'users', id: '1', attributes: { email: 'ada@lovelace.dev' } } },
|
|
203
|
-
{ op: 'remove', ref: { type: 'users', id: '2' } },
|
|
204
|
-
{ op: 'update', ref: { type: 'users', id: '1', relationship: 'team' }, data: { type: 'teams', id: '9' } },
|
|
205
|
-
{ op: 'add', ref: { type: 'teams', id: '9', relationship: 'members' }, data: [{ type: 'users', id: '1' }] },
|
|
206
|
-
],
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const response: JsonApiAtomicResultsDocument = {
|
|
210
|
-
'atomic:results': [{ data: { type: 'users', id: '1' } }, {}, {}, {}, {}],
|
|
211
|
-
}
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
`JsonApiAtomicRequestDocument` and `JsonApiAtomicResponseDocument` are the
|
|
215
|
-
named aliases for the request and response side of the extension; the latter
|
|
216
|
-
is a union of `JsonApiAtomicResultsDocument` and `JsonApiErrorDocument`, since
|
|
217
|
-
a batch of operations can still fail as a whole.
|
|
218
|
-
|
|
219
|
-
#### Extension And `@`-Members
|
|
220
|
-
|
|
221
|
-
`JsonApiExtensionMembers` and `JsonApiAtMembers` compose extension-namespaced
|
|
222
|
-
(`'ext:member'`) and meta (`'@member'`) properties through intersections, for
|
|
223
|
-
custom or third-party JSON:API extensions beyond Atomic Operations.
|
|
224
|
-
|
|
225
|
-
```ts
|
|
226
|
-
import type { JsonApiExtensionMembers, JsonApiResourceObject } from '../../../shared/application/http/json-api.js'
|
|
227
|
-
|
|
228
|
-
type VersionedResource = JsonApiResourceObject<'users'> &
|
|
229
|
-
JsonApiExtensionMembers<'version', { id: string }>
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
> **Hint**
|
|
233
|
-
> These declarations only provide compile-time structure. Rules that depend on
|
|
234
|
-
> runtime values, URI validity, document-wide uniqueness, or member-name
|
|
235
|
-
> character validation still require explicit checks in the adapter.
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
### JSON Web Tokens
|
|
2
|
-
|
|
3
|
-
`json-web-token.ts` declares a type-only implementation of the JOSE and JWT
|
|
4
|
-
family of RFCs (JWS, JWE, JWK, JWT, and related extensions such as DPoP and
|
|
5
|
-
selective disclosure). It lets an adapter describe tokens and keys precisely
|
|
6
|
-
without depending on a specific JOSE library's own types.
|
|
7
|
-
|
|
8
|
-
This module has no runtime implementation. Pair it with a concrete JOSE
|
|
9
|
-
library (for signing, encryption, or verification) and use these types to
|
|
10
|
-
annotate its inputs and outputs.
|
|
11
|
-
|
|
12
|
-
#### Branded Wire Values
|
|
13
|
-
|
|
14
|
-
Wire-format primitives are branded so a plain `string` cannot be passed where
|
|
15
|
-
an encoded value is expected: `Base64Url`, `Base64`, `NumericDate`,
|
|
16
|
-
`StringOrURI`, `UriString`, `MediaType`, and `KeyId`. `BinaryData` is the
|
|
17
|
-
union `ArrayBuffer | Uint8Array` used wherever a JOSE library accepts raw
|
|
18
|
-
bytes.
|
|
19
|
-
|
|
20
|
-
#### JSON Web Keys
|
|
21
|
-
|
|
22
|
-
Every JWK shares `JwkCommonParameters` (`kty`, `use`, `key_ops`, `alg`, `kid`,
|
|
23
|
-
the `x5*` certificate members, and the OpenID Federation `iat`/`nbf`/`exp`/
|
|
24
|
-
`revoked` members). `JwkKeyType` (`kty`) then selects one of five key-family
|
|
25
|
-
options, each with a public and a private variant where the family supports
|
|
26
|
-
one:
|
|
27
|
-
|
|
28
|
-
| `kty` | Curve/size parameter | Public type | Private type |
|
|
29
|
-
| --- | --- | --- | --- |
|
|
30
|
-
| `'EC'` | `EcCurve` (`P-256`, `P-384`, `P-521`, `secp256k1`) | `EcPublicJwk` (`x`, `y`) | `EcPrivateJwk` (adds `d`) |
|
|
31
|
-
| `'RSA'` | — | `RsaPublicJwk` (`n`, `e`) | `RsaPrivateJwk` (adds `d`, `p`, `q`, `dp`, `dq`, `qi`, `oth`) |
|
|
32
|
-
| `'oct'` | — | — (symmetric only) | `OctJwk` (`k`) |
|
|
33
|
-
| `'OKP'` | `OkpCurve` (`Ed25519`, `Ed448`, `X25519`, `X448`) | `OkpPublicJwk` (`x`) | `OkpPrivateJwk` (adds `d`) |
|
|
34
|
-
| `'AKP'` | `MldsaAlgorithm` (`ML-DSA-44`/`65`/`87`) | `AkpPublicJwk` (`pub`) | `AkpPrivateJwk` (adds `priv`) |
|
|
35
|
-
|
|
36
|
-
`PublicJwk` and `PrivateJwk` are the unions across all five families;
|
|
37
|
-
`JsonWebKey` is `PublicJwk | PrivateJwk`.
|
|
38
|
-
|
|
39
|
-
```ts
|
|
40
|
-
import type { EcPublicJwk, RsaPrivateJwk, OctJwk } from '../../../shared/application/http/json-web-token.js'
|
|
41
|
-
|
|
42
|
-
const ecKey: EcPublicJwk = { kty: 'EC', crv: 'P-256', x: '...' as Base64Url, y: '...' as Base64Url }
|
|
43
|
-
const rsaKey: RsaPrivateJwk = { kty: 'RSA', n: '...' as Base64Url, e: '...' as Base64Url, d: '...' as Base64Url }
|
|
44
|
-
const symmetricKey: OctJwk = { kty: 'oct', k: '...' as Base64Url }
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
`JsonWebKeySet` wraps a `keys` array of mixed key types; `PublicJsonWebKeySet`
|
|
48
|
-
narrows that array to public keys only, for a key set safe to publish. A
|
|
49
|
-
thumbprint (`JwkThumbprint`) is a `Base64Url`; `JwkThumbprintUri` is the
|
|
50
|
-
`urn:ietf:params:oauth:jwk-thumbprint:` URI form.
|
|
51
|
-
|
|
52
|
-
#### Headers
|
|
53
|
-
|
|
54
|
-
`JoseCommonHeaderParameters` covers the parameters shared by both header
|
|
55
|
-
kinds (`jku`, `jwk`, `kid`, the `x5*` members, `typ`, `cty`, `crit`).
|
|
56
|
-
`JwsHeaderParameters` and `JweHeaderParameters` extend it with the two
|
|
57
|
-
distinct options:
|
|
58
|
-
|
|
59
|
-
| Type | Required members | Extra members |
|
|
60
|
-
| --- | --- | --- |
|
|
61
|
-
| `JwsHeaderParameters` | `alg: JwsAlgorithm` | `b64`, `ppt`, `url`, `nonce`, `svt`, `jwt`, `client_id`, `trust_chain`, `peer_trust_chain` |
|
|
62
|
-
| `JweHeaderParameters` | `alg: JweKeyManagementAlgorithm`, `enc: JweContentEncryptionAlgorithm` | `zip`, `epk`, `apu`, `apv`, `iv`, `tag`, `p2s`, `p2c`, replicated `iss`/`sub`/`aud`, `url`, `nonce` |
|
|
63
|
-
|
|
64
|
-
`JwsHeader<CustomHeader>` and `JweHeader<CustomHeader>` intersect the
|
|
65
|
-
respective parameters with a caller-supplied custom header shape.
|
|
66
|
-
|
|
67
|
-
#### Serializations
|
|
68
|
-
|
|
69
|
-
A JWS or a JWE can be represented four ways: as a single delimited string
|
|
70
|
-
(compact), or as JSON with either one signer/recipient inlined (flattened) or
|
|
71
|
-
several (general).
|
|
72
|
-
|
|
73
|
-
| Type | Kind | Shape |
|
|
74
|
-
| --- | --- | --- |
|
|
75
|
-
| `CompactJws` | JWS, compact | `` `${string}.${string}.${string}` `` branded string |
|
|
76
|
-
| `GeneralJwsJsonSerialization` | JWS, JSON, general | `payload` + `signatures[]` |
|
|
77
|
-
| `FlattenedJwsJsonSerialization` | JWS, JSON, flattened | `payload` + one signature inlined |
|
|
78
|
-
| `CompactJwe` | JWE, compact | `` `${string}.${string}.${string}.${string}.${string}` `` branded string |
|
|
79
|
-
| `GeneralJweJsonSerialization` | JWE, JSON, general | shared members + `recipients[]` |
|
|
80
|
-
| `FlattenedJweJsonSerialization` | JWE, JSON, flattened | shared members + one recipient inlined |
|
|
81
|
-
|
|
82
|
-
`JwsJsonSerialization` and `JweJsonSerialization` are the general/flattened
|
|
83
|
-
unions for each. `UnsecuredCompactJws` types the `alg: 'none'` compact form
|
|
84
|
-
(`` `${string}.${string}.` `` — an empty signature segment).
|
|
85
|
-
|
|
86
|
-
```ts
|
|
87
|
-
import type { CompactJws, FlattenedJwsJsonSerialization } from '../../../shared/application/http/json-web-token.js'
|
|
88
|
-
|
|
89
|
-
const compact = 'eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln' as CompactJws
|
|
90
|
-
|
|
91
|
-
const flattened: FlattenedJwsJsonSerialization = {
|
|
92
|
-
payload: 'eyJzdWIiOiIxIn0' as Base64Url,
|
|
93
|
-
protected: 'eyJhbGciOiJFUzI1NiJ9' as Base64Url,
|
|
94
|
-
signature: 'c2ln' as Base64Url,
|
|
95
|
-
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
`DecodedJws` and `DecodedJwe` describe a decoded token's structure;
|
|
99
|
-
`DecodedJwt` is their union, discriminated by a `kind: 'JWS' | 'JWE'` tag.
|
|
100
|
-
`CompactJwt` is `CompactJws | CompactJwe`, matching how RFC 7519 allows a JWT
|
|
101
|
-
to use either serialization.
|
|
102
|
-
|
|
103
|
-
#### JWT Claims
|
|
104
|
-
|
|
105
|
-
`JwtRegisteredClaims` covers the seven RFC 7519 claims (`iss`, `sub`, `aud`,
|
|
106
|
-
`exp`, `nbf`, `iat`, `jti`). `IanaRegisteredJwtClaims` extends it with every
|
|
107
|
-
claim name currently registered by IANA (OpenID Connect profile claims, SIP,
|
|
108
|
-
CDNI, GNAP, EAT, RATS, RFC 9901 selective-disclosure claims, OpenID
|
|
109
|
-
Federation claims, and more), so `JwtClaims<CustomClaims>` covers arbitrary
|
|
110
|
-
custom claims through intersection while still typing every standard one.
|
|
111
|
-
|
|
112
|
-
Four ready-made profiles fix `JwtRegisteredClaims` members to `required`
|
|
113
|
-
where the profile mandates them, as distinct implementation options for
|
|
114
|
-
common token kinds:
|
|
115
|
-
|
|
116
|
-
| Type | Profile | Required beyond the base seven |
|
|
117
|
-
| --- | --- | --- |
|
|
118
|
-
| `OpenIdConnectIdTokenClaims` | OIDC ID Token | `iss`, `sub`, `aud`, `exp`, `iat` |
|
|
119
|
-
| `OAuth2JwtAccessTokenClaims` | RFC 9068 OAuth 2.0 access token | `iss`, `exp`, `aud`, `sub`, `client_id`, `iat`, `jti` |
|
|
120
|
-
| `DpopProofClaims` | RFC 9449 DPoP proof | `jti`, `htm`, `htu`, `iat` |
|
|
121
|
-
| `SdJwtClaims` | RFC 9901 SD-JWT | none beyond `IanaRegisteredJwtClaims`, adds `_sd`/`_sd_alg` |
|
|
122
|
-
|
|
123
|
-
```ts title="users/adapters/verify-access-token.ts"
|
|
124
|
-
import {
|
|
125
|
-
JwsVerifier,
|
|
126
|
-
OAuth2JwtAccessTokenClaims,
|
|
127
|
-
CompactJws,
|
|
128
|
-
JsonWebKey,
|
|
129
|
-
} from '../../shared/application/http/json-web-token.js'
|
|
130
|
-
|
|
131
|
-
export function verifyAccessToken(
|
|
132
|
-
verifier: JwsVerifier,
|
|
133
|
-
token: CompactJws,
|
|
134
|
-
key: JsonWebKey,
|
|
135
|
-
) {
|
|
136
|
-
return verifier.verify<OAuth2JwtAccessTokenClaims>(token, key)
|
|
137
|
-
}
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
`SdJwtDisclosure`, `SdJwt`, and `SdJwtPresentation` brand the three distinct
|
|
141
|
-
string forms used by selective disclosure: a single disclosure, an issued
|
|
142
|
-
SD-JWT, and a presentation with disclosures appended.
|
|
143
|
-
|
|
144
|
-
#### Service Contracts
|
|
145
|
-
|
|
146
|
-
An adapter implements these interfaces against a concrete JOSE library. Each
|
|
147
|
-
one exposes a distinct operation:
|
|
148
|
-
|
|
149
|
-
| Contract | Method(s) | Responsibility |
|
|
150
|
-
| --- | --- | --- |
|
|
151
|
-
| `JwtDecoder` | `decode()` | Parse a compact token into a `DecodedJwt` without necessarily validating it. |
|
|
152
|
-
| `JwsSigner` | `signCompact()`, `signFlattened()` | Produce a JWS in either serialization. |
|
|
153
|
-
| `JwsVerifier` | `verify()` | Validate a JWS and return a `JwtValidationResult`. |
|
|
154
|
-
| `JweEncrypter` | `encryptCompact()`, `encryptFlattened()` | Produce a JWE in either serialization. |
|
|
155
|
-
| `JweDecrypter` | `decrypt()` | Decrypt a JWE back into `BinaryData`. |
|
|
156
|
-
| `JwkThumbprinter` | `thumbprint()`, `thumbprintUri()` | Compute an RFC 7638 thumbprint, plain or as a URI. |
|
|
157
|
-
| `JwksResolver` | `resolve()`, `select()` | Fetch a JWK Set and pick the key matching a header's `kid`/`x5t`/`x5t#S256`. |
|
|
158
|
-
|
|
159
|
-
```ts
|
|
160
|
-
import type { JwkThumbprinter, JsonWebKey } from '../../../shared/application/http/json-web-token.js'
|
|
161
|
-
|
|
162
|
-
function fingerprint(thumbprinter: JwkThumbprinter, key: JsonWebKey) {
|
|
163
|
-
return thumbprinter.thumbprintUri(key)
|
|
164
|
-
}
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
#### Validating A Token
|
|
168
|
-
|
|
169
|
-
`JwtValidationResult` is the outcome of checking a token against a
|
|
170
|
-
`JwtValidationPolicy` (`issuers`, `audiences`, `subjects`, `algorithms`,
|
|
171
|
-
`requiredClaims`, `clockToleranceSeconds`, `maxTokenAgeSeconds`, `typ`). It
|
|
172
|
-
has exactly two implementation options, discriminated by `valid`:
|
|
173
|
-
|
|
174
|
-
| Type | `valid` | Payload |
|
|
175
|
-
| --- | --- | --- |
|
|
176
|
-
| `JwtValidationSuccess` | `true` | `value: DecodedJws<Claims>` |
|
|
177
|
-
| `JwtValidationFailure` | `false` | `code`, `message`, optional `cause` |
|
|
178
|
-
|
|
179
|
-
`JwtValidationFailure['code']` enumerates the recognized rejection reasons:
|
|
180
|
-
`'malformed'`, `'unsupported_serialization'`, `'unsupported_algorithm'`,
|
|
181
|
-
`'invalid_signature'`, `'decryption_failed'`, `'expired'`, `'not_active'`,
|
|
182
|
-
`'issued_in_future'`, `'issuer_mismatch'`, `'audience_mismatch'`,
|
|
183
|
-
`'subject_mismatch'`, `'missing_claim'`, `'critical_header_unsupported'`, and
|
|
184
|
-
`'policy_rejected'` (plus an open string for adapter-specific codes).
|
|
185
|
-
|
|
186
|
-
```ts
|
|
187
|
-
import type { JwtValidationResult } from '../../../shared/application/http/json-web-token.js'
|
|
188
|
-
|
|
189
|
-
function describeResult(result: JwtValidationResult): string {
|
|
190
|
-
if (result.valid) {
|
|
191
|
-
return `ok, subject=${result.value.claims.sub}`
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return `rejected: ${result.code}`
|
|
195
|
-
}
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
#### Registry Metadata
|
|
199
|
-
|
|
200
|
-
`JoseAlgorithmRegistryEntry`, `JoseHeaderParameterRegistryEntry`, and
|
|
201
|
-
`JwtClaimRegistryEntry` type the shape of a row describing an algorithm, a
|
|
202
|
-
header parameter, or a claim, for adapters that want to render or validate
|
|
203
|
-
against IANA's own registry data instead of hardcoding the unions above.
|
|
204
|
-
|
|
205
|
-
> **Hint**
|
|
206
|
-
> This module has no runtime implementation. Choose a JOSE library, then use
|
|
207
|
-
> `JwsSigner`/`JwsVerifier`/`JweEncrypter`/`JweDecrypter`/`JwtDecoder` to
|
|
208
|
-
> annotate its inputs and outputs so the rest of the codebase stays independent
|
|
209
|
-
> from that library's own types.
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
### Open Graph
|
|
2
|
-
|
|
3
|
-
`opengraph.ts` declares a type-only implementation of the
|
|
4
|
-
[Open Graph protocol](https://ogp.me/), the Twitter Card meta tags, and
|
|
5
|
-
Facebook's compatibility extensions. It is used when an adapter needs to build
|
|
6
|
-
or read the social-sharing metadata of a page.
|
|
7
|
-
|
|
8
|
-
This module has no runtime values or implementations, including no HTML
|
|
9
|
-
rendering. Use it to build the data and a separate template or renderer to
|
|
10
|
-
emit the actual `<meta>` tags.
|
|
11
|
-
|
|
12
|
-
#### Base Shape
|
|
13
|
-
|
|
14
|
-
Every Open Graph object shares `OpenGraphBase<TType>`: the required `title`,
|
|
15
|
-
`type`, `images` (`OneOrMore<OpenGraphImage>`, first has precedence), and
|
|
16
|
-
`url`, plus the optional `audio`, `description`, `determiner`, `locale`,
|
|
17
|
-
`alternateLocales`, `siteName`, and `videos`. `OpenGraphObjectExtensions`
|
|
18
|
-
adds an optional `extensions` map for CURIE-namespaced custom properties
|
|
19
|
-
(`'product:color'`, etc.), kept as opaque strings.
|
|
20
|
-
|
|
21
|
-
#### Standard Object Types
|
|
22
|
-
|
|
23
|
-
`OpenGraphType` selects one of thirteen standard types
|
|
24
|
-
(`OpenGraphStandardType`) or a custom CURIE type
|
|
25
|
-
(`OpenGraphCustomType`, e.g. `'product:item'`). Each standard type has its
|
|
26
|
-
own interface adding the fields that type requires:
|
|
27
|
-
|
|
28
|
-
| `type` | Interface | Extra fields |
|
|
29
|
-
| --- | --- | --- |
|
|
30
|
-
| `'website'` | `OpenGraphWebsite` | none |
|
|
31
|
-
| `'article'` | `OpenGraphArticle` | `publishedTime`, `modifiedTime`, `expirationTime`, `authors`, `section`, `tags` |
|
|
32
|
-
| `'book'` | `OpenGraphBook` | `authors`, `isbn`, `releaseDate`, `tags` |
|
|
33
|
-
| `'profile'` | `OpenGraphProfile` | `firstName`, `lastName`, `username`, `gender` |
|
|
34
|
-
| `'music.song'` | `OpenGraphMusicSong` | `duration`, `albums`, `musicians` |
|
|
35
|
-
| `'music.album'` | `OpenGraphMusicAlbum` | `songs`, `musicians`, `releaseDate` |
|
|
36
|
-
| `'music.playlist'` | `OpenGraphMusicPlaylist` | `songs`, `creator` |
|
|
37
|
-
| `'music.radio_station'` | `OpenGraphMusicRadioStation` | `creator` |
|
|
38
|
-
| `'video.movie'` | `OpenGraphVideoMovie` | `actors`, `directors`, `writers`, `duration`, `releaseDate`, `tags` (`OpenGraphVideoMetadata`) |
|
|
39
|
-
| `'video.episode'` | `OpenGraphVideoEpisode` | `OpenGraphVideoMetadata` + `series` |
|
|
40
|
-
| `'video.tv_show'` | `OpenGraphVideoTvShow` | `OpenGraphVideoMetadata` |
|
|
41
|
-
| `'video.other'` | `OpenGraphVideoOther` | `OpenGraphVideoMetadata` |
|
|
42
|
-
| `'payment.link'` | `OpenGraphPaymentLink` | `paymentDescription`, `currency`, `amount`, `expiresAt`, `paymentStatus`, `paymentId`, `successUrl` (marked beta by the protocol) |
|
|
43
|
-
| CURIE type | `OpenGraphCustomObject` | none beyond the base shape |
|
|
44
|
-
|
|
45
|
-
`OpenGraphMetadata` is the union of all fourteen. Building one selects the
|
|
46
|
-
type through the discriminant and, from there, TypeScript narrows to that
|
|
47
|
-
type's own extra fields.
|
|
48
|
-
|
|
49
|
-
```ts title="users/adapters/user-profile-metadata.ts"
|
|
50
|
-
import { OpenGraphProfile } from '../../shared/application/http/opengraph.js'
|
|
51
|
-
|
|
52
|
-
export function buildProfileMetadata(username: string): OpenGraphProfile {
|
|
53
|
-
return {
|
|
54
|
-
type: 'profile',
|
|
55
|
-
title: username,
|
|
56
|
-
url: `https://example.com/users/${username}`,
|
|
57
|
-
images: [{ url: `https://example.com/users/${username}/avatar.png` }],
|
|
58
|
-
username,
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
```ts title="users/adapters/article-metadata.ts"
|
|
64
|
-
import { OpenGraphArticle } from '../../shared/application/http/opengraph.js'
|
|
65
|
-
|
|
66
|
-
export function buildArticleMetadata(slug: string): OpenGraphArticle {
|
|
67
|
-
return {
|
|
68
|
-
type: 'article',
|
|
69
|
-
title: 'How context ports work',
|
|
70
|
-
url: `https://example.com/blog/${slug}`,
|
|
71
|
-
images: [{ url: `https://example.com/blog/${slug}/cover.png` }],
|
|
72
|
-
publishedTime: new Date().toISOString(),
|
|
73
|
-
tags: ['architecture', 'typescript'],
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
`OpenGraphPaymentStatus` (`'PENDING' | 'PAID' | 'FAILED' | 'EXPIRED'`) types
|
|
79
|
-
`OpenGraphPaymentLink['paymentStatus']`.
|
|
80
|
-
|
|
81
|
-
#### Twitter Cards
|
|
82
|
-
|
|
83
|
-
`TwitterCardType` selects one of four card layouts. `TwitterCardBase<TCard>`
|
|
84
|
-
carries the members every card shares (`site`, `siteId`, `creator`,
|
|
85
|
-
`creatorId`, `title`, `description`); each concrete card adds the fields that
|
|
86
|
-
layout needs:
|
|
87
|
-
|
|
88
|
-
| `card` | Interface | Extra/required fields |
|
|
89
|
-
| --- | --- | --- |
|
|
90
|
-
| `'summary'` | `TwitterSummaryCard` | optional `image` |
|
|
91
|
-
| `'summary_large_image'` | `TwitterSummaryLargeImageCard` | optional `image` |
|
|
92
|
-
| `'player'` | `TwitterPlayerCard` | required `image`, `player`, `playerWidth`, `playerHeight`; optional `playerStream`, `playerStreamContentType` |
|
|
93
|
-
| `'app'` | `TwitterAppCard` | optional `country`, `iphone`, `ipad`, `googlePlay` (each `TwitterAppPlatform`) |
|
|
94
|
-
|
|
95
|
-
`TwitterCardMetadata` is the union of all four.
|
|
96
|
-
|
|
97
|
-
```ts
|
|
98
|
-
import type { TwitterPlayerCard } from '../../../shared/application/http/opengraph.js'
|
|
99
|
-
|
|
100
|
-
const playerCard: TwitterPlayerCard = {
|
|
101
|
-
card: 'player',
|
|
102
|
-
image: { url: 'https://example.com/videos/1/thumb.png' },
|
|
103
|
-
player: 'https://example.com/videos/1/embed',
|
|
104
|
-
playerWidth: 640,
|
|
105
|
-
playerHeight: 360,
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
#### Raw Meta Tag Representation
|
|
110
|
-
|
|
111
|
-
`OpenGraphMetaTag`/`TwitterMetaTag` are the flat `property`/`content` and
|
|
112
|
-
`name`/`content` tag forms closer to the actual `<meta>` markup, for a
|
|
113
|
-
renderer that emits tags directly instead of consuming the structured
|
|
114
|
-
objects above.
|
|
115
|
-
|
|
116
|
-
| Tag family | Type | Attribute pair |
|
|
117
|
-
| --- | --- | --- |
|
|
118
|
-
| Open Graph (standard) | `OpenGraphStandardMetaTag` | `property`/`content`, e.g. `'og:title'` |
|
|
119
|
-
| Open Graph (custom) | `OpenGraphCustomMetaTag` | any `` `${string}:${string}` `` property |
|
|
120
|
-
| Twitter | `TwitterMetaTag` | `name`/`content`, e.g. `'twitter:card'` |
|
|
121
|
-
| Facebook | `FacebookMetaTag` | `PropertyMetaTag<'fb:app_id'>` |
|
|
122
|
-
| Standard HTML | `StandardHtmlMetaTag` | `'description'` \| `'theme-color'` |
|
|
123
|
-
|
|
124
|
-
`SocialMetaTag` is the union of all five families. `CanonicalLinkTag` types
|
|
125
|
-
the `<link rel="canonical">` element separately, since it is not a `<meta>`
|
|
126
|
-
tag.
|
|
127
|
-
|
|
128
|
-
#### Aggregate Document
|
|
129
|
-
|
|
130
|
-
Two document shapes cover the two stages of building a page's social
|
|
131
|
-
metadata:
|
|
132
|
-
|
|
133
|
-
| Type | Shape | Use |
|
|
134
|
-
| --- | --- | --- |
|
|
135
|
-
| `SocialMetadataDocument` | `head?`, `openGraph` (required), `twitter?`, `facebook?` | Structured data a service builds before rendering |
|
|
136
|
-
| `RawSocialMetadataDocument` | `title?`, `meta: SocialMetaTag[]`, `links?: CanonicalLinkTag[]` | The rendered, tag-list form a template consumes |
|
|
137
|
-
|
|
138
|
-
```ts title="users/adapters/social-metadata-document.ts"
|
|
139
|
-
import {
|
|
140
|
-
SocialMetadataDocument,
|
|
141
|
-
OpenGraphProfile,
|
|
142
|
-
} from '../../shared/application/http/opengraph.js'
|
|
143
|
-
|
|
144
|
-
export function buildSocialMetadata(profile: OpenGraphProfile): SocialMetadataDocument {
|
|
145
|
-
return {
|
|
146
|
-
head: { title: profile.title, description: profile.description },
|
|
147
|
-
openGraph: profile,
|
|
148
|
-
twitter: { card: 'summary', title: profile.title },
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
Converting a `SocialMetadataDocument` into a `RawSocialMetadataDocument` is
|
|
154
|
-
the responsibility of a renderer, which flattens each structured field into
|
|
155
|
-
its corresponding `SocialMetaTag` entries; that conversion is not implemented
|
|
156
|
-
by this module.
|
|
157
|
-
|
|
158
|
-
> **Hint**
|
|
159
|
-
> This module has no runtime implementation, including no HTML rendering. Use
|
|
160
|
-
> `OpenGraphMetadata` to build the data and a separate template or renderer to
|
|
161
|
-
> emit the `<meta>` tags described by `OpenGraphMetaTag`/`TwitterMetaTag`.
|