@mediaviz/sdk 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/MediaViz.js +126 -0
- package/_oauth.js +3 -0
- package/admin.js +93 -0
- package/ai_model_credits.js +22 -0
- package/company.js +54 -0
- package/curated_albums.js +85 -0
- package/custom_albums.js +78 -0
- package/dist/sdk.cjs +1976 -0
- package/dist/sdk.esm.js +1947 -0
- package/dist/sdk.umd.js +1982 -0
- package/email_tokens.js +64 -0
- package/errors.js +81 -0
- package/health.js +20 -0
- package/index.js +21 -0
- package/keywords.js +123 -0
- package/oauth/.prettierrc +6 -0
- package/oauth/README.md +76 -0
- package/oauth/browser-smoke-test.html +45 -0
- package/oauth/implementation_plan.json +106 -0
- package/oauth/package-lock.json +5236 -0
- package/oauth/package.json +28 -0
- package/oauth/rollup.config.js +21 -0
- package/oauth/smoke-test.js +27 -0
- package/oauth/spec.md +187 -0
- package/oauth/src/__tests__/browser-smoke-test.test.js +38 -0
- package/oauth/src/__tests__/client.test.js +556 -0
- package/oauth/src/__tests__/errors.test.js +73 -0
- package/oauth/src/__tests__/http.test.js +102 -0
- package/oauth/src/__tests__/index.test.js +53 -0
- package/oauth/src/__tests__/package-fields.test.js +29 -0
- package/oauth/src/__tests__/pkce.test.js +55 -0
- package/oauth/src/__tests__/rollup-build.test.js +58 -0
- package/oauth/src/__tests__/smoke-test.test.js +26 -0
- package/oauth/src/__tests__/types.test.js +29 -0
- package/oauth/src/client.js +180 -0
- package/oauth/src/errors.js +32 -0
- package/oauth/src/http.js +52 -0
- package/oauth/src/index.js +7 -0
- package/oauth/src/pkce.js +50 -0
- package/oauth/src/types.js +67 -0
- package/oauth_authorization.js +53 -0
- package/oauth_clients.js +18 -0
- package/oauth_login.js +24 -0
- package/oauth_token.js +30 -0
- package/package.json +27 -0
- package/person.js +54 -0
- package/photos.js +106 -0
- package/photoupload.js +55 -0
- package/projects.js +191 -0
- package/rollup.config.js +12 -0
- package/search.js +99 -0
- package/users.js +137 -0
package/email_tokens.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { handleResponse } from './errors.js';
|
|
2
|
+
|
|
3
|
+
function stripUndef(o) { const r = {}; for (const k in o) if (o[k] !== undefined) r[k] = o[k]; return r; }
|
|
4
|
+
|
|
5
|
+
export class EmailTokens {
|
|
6
|
+
constructor(ctx) { this._ctx = ctx; }
|
|
7
|
+
|
|
8
|
+
async requestEmailVerification({ email } = {}) {
|
|
9
|
+
let path = `/api/v1/request-email-verification`;
|
|
10
|
+
const query = new URLSearchParams();
|
|
11
|
+
if (email !== undefined) (Array.isArray(email) ? email : [email]).forEach(v => query.append('email', v));
|
|
12
|
+
const qs = query.toString();
|
|
13
|
+
if (qs) path += '?' + qs;
|
|
14
|
+
const resp = await fetch(this._ctx.baseUrl + path, { method: 'POST' });
|
|
15
|
+
return handleResponse(resp);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async verifyEmail(token) {
|
|
19
|
+
const resp = await fetch(this._ctx.baseUrl + `/api/v1/verify-email/${encodeURIComponent(token)}`, { method: 'POST' });
|
|
20
|
+
return handleResponse(resp);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async requestPasswordReset({ email } = {}) {
|
|
24
|
+
let path = `/api/v1/request-password-reset`;
|
|
25
|
+
const query = new URLSearchParams();
|
|
26
|
+
if (email !== undefined) (Array.isArray(email) ? email : [email]).forEach(v => query.append('email', v));
|
|
27
|
+
const qs = query.toString();
|
|
28
|
+
if (qs) path += '?' + qs;
|
|
29
|
+
const resp = await fetch(this._ctx.baseUrl + path, { method: 'POST' });
|
|
30
|
+
return handleResponse(resp);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async validateToken(token) {
|
|
34
|
+
const body = stripUndef({
|
|
35
|
+
token: token,
|
|
36
|
+
});
|
|
37
|
+
const resp = await fetch(this._ctx.baseUrl + `/api/v1/validate-token`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify(body),
|
|
41
|
+
});
|
|
42
|
+
return handleResponse(resp);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async resetPassword(token, newPassword) {
|
|
46
|
+
const body = stripUndef({
|
|
47
|
+
token: token,
|
|
48
|
+
new_password: newPassword,
|
|
49
|
+
});
|
|
50
|
+
const resp = await fetch(this._ctx.baseUrl + `/api/v1/reset-password`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify(body),
|
|
54
|
+
});
|
|
55
|
+
return handleResponse(resp);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async deleteUserEmailTokens(userId) {
|
|
59
|
+
this._ctx.requireTokens();
|
|
60
|
+
const path = `/api/v1/admin/email_tokens/by_user/${encodeURIComponent(userId)}`;
|
|
61
|
+
const { data } = await this._ctx.client.request(path, 'DELETE', this._ctx.accessToken, this._ctx.refreshToken);
|
|
62
|
+
return data;
|
|
63
|
+
}
|
|
64
|
+
}
|
package/errors.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Auto-generated — do not edit
|
|
2
|
+
|
|
3
|
+
export class ApiError extends Error {
|
|
4
|
+
constructor(message, status, requestId, body) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'ApiError';
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.requestId = requestId;
|
|
9
|
+
this.body = body;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class ValidationError extends ApiError {
|
|
14
|
+
constructor(body, status, requestId) {
|
|
15
|
+
const detail = body.detail ?? [];
|
|
16
|
+
const message = Array.isArray(detail)
|
|
17
|
+
? detail.map(d => `${d.loc.join('.')}: ${d.msg}`).join('; ')
|
|
18
|
+
: String(detail);
|
|
19
|
+
super(message, status, requestId, body);
|
|
20
|
+
this.name = 'ValidationError';
|
|
21
|
+
this.fieldErrors = Array.isArray(detail)
|
|
22
|
+
? detail.map(d => ({ loc: d.loc, msg: d.msg, type: d.type }))
|
|
23
|
+
: [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class NotFoundError extends ApiError {
|
|
28
|
+
constructor(body, status, requestId) {
|
|
29
|
+
super(body.detail ?? 'Resource not found', status, requestId, body);
|
|
30
|
+
this.name = 'NotFoundError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class RateLimitError extends ApiError {
|
|
35
|
+
constructor(body, status, requestId, headers) {
|
|
36
|
+
super(body.detail ?? 'Rate limited', status, requestId, body);
|
|
37
|
+
this.name = 'RateLimitError';
|
|
38
|
+
this.retryAfter = parseInt(headers.get('retry-after') ?? '', 10) || null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class ServerError extends ApiError {
|
|
43
|
+
constructor(body, status, requestId) {
|
|
44
|
+
super(body.detail ?? 'Internal server error', status, requestId, body);
|
|
45
|
+
this.name = 'ServerError';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function handleResponse(response) {
|
|
50
|
+
const requestId = response.headers.get('x-request-id');
|
|
51
|
+
|
|
52
|
+
if (response.ok) {
|
|
53
|
+
return response.status === 204 ? null : response.json();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let body;
|
|
57
|
+
try {
|
|
58
|
+
body = await response.json();
|
|
59
|
+
} catch {
|
|
60
|
+
body = { detail: response.statusText };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
switch (response.status) {
|
|
64
|
+
case 422:
|
|
65
|
+
throw new ValidationError(body, response.status, requestId);
|
|
66
|
+
case 404:
|
|
67
|
+
throw new NotFoundError(body, response.status, requestId);
|
|
68
|
+
case 429:
|
|
69
|
+
throw new RateLimitError(body, response.status, requestId, response.headers);
|
|
70
|
+
default:
|
|
71
|
+
if (response.status >= 500) {
|
|
72
|
+
throw new ServerError(body, response.status, requestId);
|
|
73
|
+
}
|
|
74
|
+
throw new ApiError(
|
|
75
|
+
body.detail ?? 'Unknown error',
|
|
76
|
+
response.status,
|
|
77
|
+
requestId,
|
|
78
|
+
body
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
package/health.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { handleResponse } from './errors.js';
|
|
2
|
+
|
|
3
|
+
export class Health {
|
|
4
|
+
constructor(ctx) { this._ctx = ctx; }
|
|
5
|
+
|
|
6
|
+
async healthCheck() {
|
|
7
|
+
const resp = await fetch(this._ctx.baseUrl + `/api/v1/health/`, { method: 'GET' });
|
|
8
|
+
return handleResponse(resp);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async livenessCheck() {
|
|
12
|
+
const resp = await fetch(this._ctx.baseUrl + `/api/v1/health/live/`, { method: 'GET' });
|
|
13
|
+
return handleResponse(resp);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async readinessCheck() {
|
|
17
|
+
const resp = await fetch(this._ctx.baseUrl + `/api/v1/health/ready`, { method: 'GET' });
|
|
18
|
+
return handleResponse(resp);
|
|
19
|
+
}
|
|
20
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { MediaViz } from './MediaViz.js';
|
|
2
|
+
export * from './errors.js';
|
|
3
|
+
export * from './_oauth.js';
|
|
4
|
+
export * from './admin.js';
|
|
5
|
+
export * from './ai_model_credits.js';
|
|
6
|
+
export * from './company.js';
|
|
7
|
+
export * from './curated_albums.js';
|
|
8
|
+
export * from './custom_albums.js';
|
|
9
|
+
export * from './email_tokens.js';
|
|
10
|
+
export * from './health.js';
|
|
11
|
+
export * from './keywords.js';
|
|
12
|
+
export * from './oauth_authorization.js';
|
|
13
|
+
export * from './oauth_clients.js';
|
|
14
|
+
export * from './oauth_login.js';
|
|
15
|
+
export * from './oauth_token.js';
|
|
16
|
+
export * from './person.js';
|
|
17
|
+
export * from './photos.js';
|
|
18
|
+
export * from './photoupload.js';
|
|
19
|
+
export * from './projects.js';
|
|
20
|
+
export * from './search.js';
|
|
21
|
+
export * from './users.js';
|
package/keywords.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
function stripUndef(o) { const r = {}; for (const k in o) if (o[k] !== undefined) r[k] = o[k]; return r; }
|
|
2
|
+
|
|
3
|
+
export class Keywords {
|
|
4
|
+
constructor(ctx) { this._ctx = ctx; }
|
|
5
|
+
|
|
6
|
+
async createKeywordFilteringList(name, projectList = undefined) {
|
|
7
|
+
this._ctx.requireTokens();
|
|
8
|
+
const path = `/api/v1/keyword/`;
|
|
9
|
+
const body = stripUndef({
|
|
10
|
+
name: name,
|
|
11
|
+
project_list: projectList,
|
|
12
|
+
});
|
|
13
|
+
const { data } = await this._ctx.client.request(path, 'POST', this._ctx.accessToken, this._ctx.refreshToken, body);
|
|
14
|
+
return data;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async getUserKeywordFilteringLists() {
|
|
18
|
+
this._ctx.requireTokens();
|
|
19
|
+
const path = `/api/v1/keyword/user`;
|
|
20
|
+
const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
|
|
21
|
+
return data;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async getKeywordFilteringListAndProjectsById(keywordListId) {
|
|
25
|
+
this._ctx.requireTokens();
|
|
26
|
+
const path = `/api/v1/keyword/${encodeURIComponent(keywordListId)}`;
|
|
27
|
+
const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
|
|
28
|
+
return data;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async getKeywordFilteringListById(keywordListId) {
|
|
32
|
+
this._ctx.requireTokens();
|
|
33
|
+
const path = `/api/v1/keyword/list/${encodeURIComponent(keywordListId)}`;
|
|
34
|
+
const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
|
|
35
|
+
return data;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async getExistingKeywordFilteringListByProject(projectTableName) {
|
|
39
|
+
this._ctx.requireTokens();
|
|
40
|
+
const path = `/api/v1/keyword/project/${encodeURIComponent(projectTableName)}`;
|
|
41
|
+
const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
|
|
42
|
+
return data;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getDefaultKeywordFilteringListByProject(projectTableName) {
|
|
46
|
+
this._ctx.requireTokens();
|
|
47
|
+
const path = `/api/v1/keyword/project/${encodeURIComponent(projectTableName)}/default`;
|
|
48
|
+
const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async updateKeywordFilteringListLabels(keywordListId, listKeywordsToInclude, listKeywordsToExclude) {
|
|
53
|
+
this._ctx.requireTokens();
|
|
54
|
+
const path = `/api/v1/keyword/${encodeURIComponent(keywordListId)}`;
|
|
55
|
+
const body = stripUndef({
|
|
56
|
+
list_keywords_to_include: listKeywordsToInclude,
|
|
57
|
+
list_keywords_to_exclude: listKeywordsToExclude,
|
|
58
|
+
});
|
|
59
|
+
const { data } = await this._ctx.client.request(path, 'PUT', this._ctx.accessToken, this._ctx.refreshToken, body);
|
|
60
|
+
return data;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async updateKeywordFilteringListDetails(keywordListId, { name, projectList } = {}) {
|
|
64
|
+
this._ctx.requireTokens();
|
|
65
|
+
const path = `/api/v1/keyword/details/${encodeURIComponent(keywordListId)}`;
|
|
66
|
+
const body = stripUndef({
|
|
67
|
+
name: name,
|
|
68
|
+
project_list: projectList,
|
|
69
|
+
});
|
|
70
|
+
const { data } = await this._ctx.client.request(path, 'PUT', this._ctx.accessToken, this._ctx.refreshToken, body);
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async addProjectsToKeywordFilteringList(keywordListId, { projectIds } = {}) {
|
|
75
|
+
this._ctx.requireTokens();
|
|
76
|
+
let path = `/api/v1/keyword/${encodeURIComponent(keywordListId)}/projects`;
|
|
77
|
+
const query = new URLSearchParams();
|
|
78
|
+
if (projectIds !== undefined) (Array.isArray(projectIds) ? projectIds : [projectIds]).forEach(v => query.append('project_ids', v));
|
|
79
|
+
const qs = query.toString();
|
|
80
|
+
if (qs) path += '?' + qs;
|
|
81
|
+
const { data } = await this._ctx.client.request(path, 'POST', this._ctx.accessToken, this._ctx.refreshToken);
|
|
82
|
+
return data;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async requestKeywordListExport(keywordListId) {
|
|
86
|
+
this._ctx.requireTokens();
|
|
87
|
+
const path = `/api/v1/keyword/export/${encodeURIComponent(keywordListId)}`;
|
|
88
|
+
const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
|
|
89
|
+
return data;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async requestKeywordListExportStatus(keywordListId) {
|
|
93
|
+
this._ctx.requireTokens();
|
|
94
|
+
const path = `/api/v1/keyword/export_status/${encodeURIComponent(keywordListId)}`;
|
|
95
|
+
const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
|
|
96
|
+
return data;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async getKeywordsAndIds() {
|
|
100
|
+
this._ctx.requireTokens();
|
|
101
|
+
const path = `/api/v1/keyword/all_keywords/id/label`;
|
|
102
|
+
const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
|
|
103
|
+
return data;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async removeProjectsFromKeywordFilteringList(keywordListId, { projectIds } = {}) {
|
|
107
|
+
this._ctx.requireTokens();
|
|
108
|
+
let path = `/api/v1/keyword/${encodeURIComponent(keywordListId)}/projects`;
|
|
109
|
+
const query = new URLSearchParams();
|
|
110
|
+
if (projectIds !== undefined) (Array.isArray(projectIds) ? projectIds : [projectIds]).forEach(v => query.append('project_ids', v));
|
|
111
|
+
const qs = query.toString();
|
|
112
|
+
if (qs) path += '?' + qs;
|
|
113
|
+
const { data } = await this._ctx.client.request(path, 'DELETE', this._ctx.accessToken, this._ctx.refreshToken);
|
|
114
|
+
return data;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async deleteKeywordFilteringListById(keywordListId) {
|
|
118
|
+
this._ctx.requireTokens();
|
|
119
|
+
const path = `/api/v1/keyword/${encodeURIComponent(keywordListId)}`;
|
|
120
|
+
const { data } = await this._ctx.client.request(path, 'DELETE', this._ctx.accessToken, this._ctx.refreshToken);
|
|
121
|
+
return data;
|
|
122
|
+
}
|
|
123
|
+
}
|
package/oauth/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @yourorg/oauth-sdk
|
|
2
|
+
|
|
3
|
+
JavaScript SDK for the OAuth 2.0 Authorization Server. Implements Authorization Code + PKCE flow, token refresh, token revocation, and JWT payload decoding for confidential (server-side) clients.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @yourorg/oauth-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
const { OAuthClient } = require('@yourorg/oauth-sdk');
|
|
15
|
+
|
|
16
|
+
const oauthClient = new OAuthClient({
|
|
17
|
+
baseUrl: 'https://api.example.com',
|
|
18
|
+
clientId: process.env.OAUTH_CLIENT_ID,
|
|
19
|
+
clientSecret: process.env.OAUTH_CLIENT_SECRET,
|
|
20
|
+
redirectUri: 'https://myapp.com/callback',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Step 1: initiate login
|
|
24
|
+
app.get('/login', (req, res) => {
|
|
25
|
+
const { url, state, code_verifier } = oauthClient.generateAuthorizationUrl();
|
|
26
|
+
req.session.oauthState = state;
|
|
27
|
+
req.session.codeVerifier = code_verifier;
|
|
28
|
+
res.redirect(url);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Step 2: handle callback
|
|
32
|
+
app.get('/callback', async (req, res) => {
|
|
33
|
+
const { code, state } = req.query;
|
|
34
|
+
if (state !== req.session.oauthState) throw new Error('State mismatch');
|
|
35
|
+
|
|
36
|
+
const tokens = await oauthClient.exchangeCode(code, req.session.codeVerifier);
|
|
37
|
+
const payload = oauthClient.decodeAccessToken(tokens.access_token);
|
|
38
|
+
|
|
39
|
+
req.session.userId = payload.user_id;
|
|
40
|
+
req.session.accessToken = tokens.access_token;
|
|
41
|
+
req.session.refreshToken = tokens.refresh_token;
|
|
42
|
+
res.redirect('/dashboard');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Step 3: make authenticated requests
|
|
46
|
+
app.get('/dashboard', async (req, res) => {
|
|
47
|
+
const { data, updated_tokens } = await oauthClient.request(
|
|
48
|
+
'https://api.example.com/me',
|
|
49
|
+
'GET',
|
|
50
|
+
req.session.accessToken,
|
|
51
|
+
req.session.refreshToken,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (updated_tokens) {
|
|
55
|
+
req.session.accessToken = updated_tokens.access_token;
|
|
56
|
+
req.session.refreshToken = updated_tokens.refresh_token;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
res.json(data);
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
> **Important:** `generateAuthorizationUrl()` returns `{ url, state, code_verifier }`. You **must** persist `state` and `code_verifier` (e.g. in an encrypted server-side session or short-lived encrypted cookie) and pass `code_verifier` back to `exchangeCode()` in the callback handler. The SDK does not store any state between calls.
|
|
64
|
+
|
|
65
|
+
## Method reference
|
|
66
|
+
|
|
67
|
+
| Method | Description | Returns |
|
|
68
|
+
|--------|-------------|---------|
|
|
69
|
+
| `generateAuthorizationUrl(state?)` | Generates PKCE verifier + challenge, builds `/oauth/authorize` URL | `AuthorizationUrlResult` |
|
|
70
|
+
| `exchangeCode(code, codeVerifier, redirectUri?)` | Exchanges authorization code for tokens | `Promise<TokenResponse>` |
|
|
71
|
+
| `refreshAccessToken(refreshToken)` | Issues new tokens using a refresh token | `Promise<TokenResponse>` |
|
|
72
|
+
| `revokeToken(token, tokenTypeHint?)` | Revokes an access or refresh token (RFC 7009) | `Promise<void>` |
|
|
73
|
+
| `decodeAccessToken(accessToken)` | Decodes JWT payload without signature verification | `TokenPayload` |
|
|
74
|
+
| `request(url, method, accessToken, refreshToken, body?)` | Authenticated request with automatic 401 retry | `Promise<AuthenticatedResponse>` |
|
|
75
|
+
|
|
76
|
+
See [spec.md](../../spec.md) for full API documentation.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>OAuth SDK Browser Smoke Test</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<pre id="output"></pre>
|
|
9
|
+
<script src="dist/oauth-sdk.umd.js"></script>
|
|
10
|
+
<script>
|
|
11
|
+
(async () => {
|
|
12
|
+
const log = (msg) => {
|
|
13
|
+
document.getElementById('output').textContent += msg + '\n';
|
|
14
|
+
console.log(msg);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const { OAuthClient } = OAuthSDK;
|
|
19
|
+
const client = new OAuthClient({
|
|
20
|
+
baseUrl: 'https://example.com',
|
|
21
|
+
clientId: 'test-client-id',
|
|
22
|
+
clientSecret: 'test-client-secret',
|
|
23
|
+
redirectUri: 'https://example.com/callback',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const result = await client.generateAuthorizationUrl();
|
|
27
|
+
|
|
28
|
+
if (!result.url.includes('response_type=code')) {
|
|
29
|
+
throw new Error(`url missing response_type=code: ${result.url}`);
|
|
30
|
+
}
|
|
31
|
+
if (result.code_verifier.length !== 64) {
|
|
32
|
+
throw new Error(`code_verifier length ${result.code_verifier.length} !== 64`);
|
|
33
|
+
}
|
|
34
|
+
if (result.state.length !== 32) {
|
|
35
|
+
throw new Error(`state length ${result.state.length} !== 32`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
log('PASS');
|
|
39
|
+
} catch (err) {
|
|
40
|
+
log('FAIL: ' + err.message);
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
43
|
+
</script>
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": 1,
|
|
4
|
+
"description": "Replace Node.js crypto with Web Crypto API in pkce.js. generateCodeVerifier: use globalThis.crypto.getRandomValues(new Uint8Array(48)) + manual base64url encode + slice to 64 chars. generateCodeChallenge: use globalThis.crypto.subtle.digest('SHA-256', TextEncoder) — must become async, returns Promise<string>. generateState: use getRandomValues(new Uint8Array(16)) + hex join. Remove require('crypto'). Keep module.exports unchanged.",
|
|
5
|
+
"touch_points": [
|
|
6
|
+
{
|
|
7
|
+
"file": "sdk/javascript/src/pkce.js",
|
|
8
|
+
"location": "generateCodeVerifier",
|
|
9
|
+
"status": "replace crypto.randomBytes with getRandomValues + base64url encode"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"file": "sdk/javascript/src/pkce.js",
|
|
13
|
+
"location": "generateCodeChallenge",
|
|
14
|
+
"status": "make async, replace createHash with crypto.subtle.digest"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"file": "sdk/javascript/src/pkce.js",
|
|
18
|
+
"location": "generateState",
|
|
19
|
+
"status": "replace crypto.randomBytes with getRandomValues + hex join"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"completion_status": true
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": 2,
|
|
26
|
+
"description": "Update client.js to await the now-async generateCodeChallenge. In generateAuthorizationUrl, change the call to: const challenge = await generateCodeChallenge(verifier). The method is already async so the public signature is unchanged.",
|
|
27
|
+
"touch_points": [
|
|
28
|
+
{
|
|
29
|
+
"file": "sdk/javascript/src/client.js",
|
|
30
|
+
"location": "generateAuthorizationUrl",
|
|
31
|
+
"status": "add await before generateCodeChallenge call"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"completion_status": true
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": 3,
|
|
38
|
+
"description": "Update pkce.test.js for async generateCodeChallenge. Change the PKCE test vector assertion to use await: expect(await generateCodeChallenge(verifier)).toBe('E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM'). Mark the test function async. All other tests are sync and unchanged.",
|
|
39
|
+
"touch_points": [
|
|
40
|
+
{
|
|
41
|
+
"file": "sdk/javascript/src/__tests__/pkce.test.js",
|
|
42
|
+
"location": "test('generateCodeChallenge')",
|
|
43
|
+
"status": "add async/await to test vector assertion"
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"completion_status": true
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": 4,
|
|
50
|
+
"description": "Add Rollup build tooling. Install dev dependencies: rollup, @rollup/plugin-node-resolve, @rollup/plugin-terser. Create rollup.config.js at sdk/javascript/ with two outputs: (1) dist/oauth-sdk.umd.js — format: 'umd', name: 'OAuthSDK', exports: 'named', plugins: [terser()]; (2) dist/oauth-sdk.esm.js — format: 'esm'. Input: src/index.js. Use nodeResolve plugin to handle any internal references.",
|
|
51
|
+
"touch_points": [
|
|
52
|
+
{
|
|
53
|
+
"file": "sdk/javascript/package.json",
|
|
54
|
+
"location": "devDependencies",
|
|
55
|
+
"status": "add rollup, @rollup/plugin-node-resolve, @rollup/plugin-terser"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"file": "sdk/javascript/rollup.config.js",
|
|
59
|
+
"location": "module",
|
|
60
|
+
"status": "create with umd + esm output config"
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"completion_status": true
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": 5,
|
|
67
|
+
"description": "Update package.json fields. Add: 'module': 'dist/oauth-sdk.esm.js'; 'browser': 'dist/oauth-sdk.umd.js'; 'files': ['src/', 'dist/']; scripts 'build': 'rollup -c' and 'prepublishOnly': 'npm run build'. Keep 'main': 'src/index.js' for Node.js CJS path.",
|
|
68
|
+
"touch_points": [
|
|
69
|
+
{
|
|
70
|
+
"file": "sdk/javascript/package.json",
|
|
71
|
+
"location": "root",
|
|
72
|
+
"status": "add module, browser, files fields and build scripts"
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
"completion_status": true
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": 6,
|
|
79
|
+
"description": "Add dist/ to .gitignore in sdk/javascript/. Update smoke-test.js: add await to generateAuthorizationUrl call (wrap in async IIFE or top-level await if package is ESM). Verify node smoke-test.js still exits 0.",
|
|
80
|
+
"touch_points": [
|
|
81
|
+
{
|
|
82
|
+
"file": "sdk/javascript/.gitignore",
|
|
83
|
+
"location": "root",
|
|
84
|
+
"status": "add dist/"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"file": "sdk/javascript/smoke-test.js",
|
|
88
|
+
"location": "main assertion block",
|
|
89
|
+
"status": "confirm generateAuthorizationUrl is awaited (should already be)"
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"completion_status": true
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"id": 7,
|
|
96
|
+
"description": "Build verification. Run npm install then npm run build in sdk/javascript/. Confirm dist/oauth-sdk.umd.js and dist/oauth-sdk.esm.js are produced. Run npm test — all tests must pass. Run node smoke-test.js — must exit 0. Create sdk/javascript/browser-smoke-test.html: minimal HTML page that loads dist/oauth-sdk.umd.js via <script>, calls OAuthSDK.OAuthClient with dummy config, calls generateAuthorizationUrl(), asserts url contains 'response_type=code', logs PASS/FAIL to console.",
|
|
97
|
+
"touch_points": [
|
|
98
|
+
{
|
|
99
|
+
"file": "sdk/javascript/browser-smoke-test.html",
|
|
100
|
+
"location": "script block",
|
|
101
|
+
"status": "create browser smoke test for UMD bundle"
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
"completion_status": true
|
|
105
|
+
}
|
|
106
|
+
]
|