openvibers/openvibe.examples Commits
Developer path: an export step (roadmap WS-N task 9, the project export in production): as the project's owner, Network mints read-only export tokens; Media lists the project's sandbox files with one and refuses an upload (403); Events returns the event published in step 7 with the other. The mock mints export tokens like Network. OV_MEDIA_URL / OV_EVENTS_URL override the origins
Changes
@@@ -157,15 +157,16 @@ scoped credentials only, then rotates and revokes those credentials and cleans u| 5. grants | requests `media.object.upload`, `media.object.read`, `events.app.publish`, `events.app.read`, `codes.release.manage`; all must be approved at once || 6. media | [media-uploader](examples/media-uploader): upload a small file, read it back, delete it || 7. events | [event-subscriber](examples/event-subscriber): publish `app.<project_key>.developer_path.ran`, pull it with a cursor |--| 8. release | as the app, on OpenVibe.Codes (`OV_CODES_URL`): a sandbox release of itself goes draft → published → deprecated → revoked, checked in the public list each time |--| 9. credentials | rotates with no overlap and revokes the first credential; the old secret must be refused at `/oauth/token`, the new one must work |--| 10. cleanup | archives the project, even when an earlier step failed; the app's secret must then be refused |++| 8. export | as the project's owner: Network mints 5-minute read-only export tokens; Media lists the project's sandbox files with one and refuses an upload; Events returns the event from step 7 with the other |++| 9. release | as the app, on OpenVibe.Codes (`OV_CODES_URL`): a sandbox release of itself goes draft → published → deprecated → revoked, checked in the public list each time |++| 10. credentials | rotates with no overlap and revokes the first credential; the old secret must be refused at `/oauth/token`, the new one must work |++| 11. cleanup | archives the project, even when an earlier step failed; the app's secret must then be refused |**In CI**, on every push, `npm run developer-path:mock` runs the same code against`openvibe-sdk/testing`'s mock platform (the `developer-path` job; `test/developer-path.test.js`also checks it leaves nothing behind, never prints a secret, and still cleans up after a failure).The mock has no account API, so `scripts/developer-path-mock.js` adds register and login routes--that mint mock user tokens, and stands in for Codes' release API; every other step is the platform mock's own.++that mint mock user tokens, mints export tokens, and stands in for Codes' release API; every other step is the platform mock's own.**Against production** it runs only when a person starts it. Credentials come from theenvironment (or `./.env`) only:@@@ -181,7 +182,7 @@ It refuses to start without credentials, and refuses in CI against the productioset and `OV_NETWORK_URL` unset or `https://openvibe.network`), so it can later run in CI againstan integration environment but never against production by accident. It masks every password,secret and token in its output and prints signed URLs without their signature. Exit `0` when all--ten steps passed, `1` when one failed, `2` when it refused to start.++eleven steps passed, `1` when one failed, `2` when it refused to start.It does not yet cover webhook delivery to an external endpoint (that needs a public https host)or publishing an app release in OpenVibe.Codes.@@@ -9,7 +9,8 @@* their public origins with real RS256 tokens. It has no account API, so this adds the two routes* the flow uses, /api/auth/register and /api/auth/login, as a thin layer: register creates a mock* user and returns a Network user token for it; login returns one for an existing user (the mock-- * does not check passwords). It also stands in for OpenVibe.Codes' release API (the release step):++ * does not check passwords). It also mints Network's export tokens (the export step, signed by the++ * platform like any app token) and stands in for OpenVibe.Codes' release API (the release step):* an app token for openvibe.codes carrying codes.release.manage manages the app's own releases,* draft → published → deprecated → revoked, and the public list shows every release but drafts, like Codes.* Everything else is the platform mock's own.@@@ -63,6 +64,15 @@ function withAccounts(platform, network = PRODUCTION_NETWORK) {const user = byName.get(username.toLowerCase());return user ? reply(200, { token: platform.signUserToken(user) }) : reply(401, { error: 'Invalid credentials' });}++ // Network's export tokens (WS-N task 9): read-only app-shaped tokens for the project's owner, as Network mints them.++ const ex = url.origin === network && method === 'POST' && url.pathname.match(/^\/api\/v1\/projects\/(prj_[0-9A-Z]+)\/export-tokens$/i);++ if (ex) {++ const body = JSON.parse(init.body || '{}');++ const caps = { 'openvibe.media': ['media.object.list', 'media.object.read'], 'openvibe.events': ['events.app.read'] }[body.audience];++ if (!caps) return reply(422, { code: 'export.invalid_audience' });++ const token = platform.signAppToken(`app_${ex[1].slice(4)}`, { audience: body.audience, capabilities: caps, projectId: ex[1], env: body.env || 'production' });++ return reply(201, { access_token: token, token_type: 'Bearer', expires_in: 300, scope: caps.join(' ') });++ }if (url.origin === PRODUCTION_CODES) return codes(url, method, init);return platform.fetch(input, init);};@@@ -13,13 +13,16 @@* approved at once for the project owner inside the sandbox allowance* 6. media examples/media-uploader: upload a small file, read it back, delete it* 7. events examples/event-subscriber: publish app.<project_key>.developer_path.ran, pull it-- * 8. release as the app, on OpenVibe.Codes: a sandbox release of itself goes draft →++ * 8. export as the project's owner: Network mints read-only export tokens (5 minutes); Media++ * lists the project's sandbox files with one and refuses an upload; Events returns the++ * event from step 7 with the other (roadmap WS-N task 9, the project export)++ * 9. release as the app, on OpenVibe.Codes: a sandbox release of itself goes draft →* published → deprecated → revoked, checked in the public list at each step (a* draft is never listed; a revoked release stays listed, marked revoked)* (roadmap WS-N task 5, the app release flow in production)-- * 9. credentials rotate with no overlap, revoke the first credential; the old secret is refused++ * 10. credentials rotate with no overlap, revoke the first credential; the old secret is refused* at /oauth/token and the new one works-- * 10. cleanup archive the project (always attempted once it exists); the app's secret is++ * 11. cleanup archive the project (always attempted once it exists); the app's secret is* refused afterwards** The same flow runs in CI against openvibe-sdk/testing's mock platform (scripts/developer-path-mock.js,@@@ -50,8 +53,10 @@ const { createSubscriber, createCursorStore, loadConfig: subscriberConfig } = reconst PRODUCTION_NETWORK = 'https://openvibe.network';const PRODUCTION_CODES = 'https://openvibe.codes';++const PRODUCTION_MEDIA = 'https://openvibe.media';++const PRODUCTION_EVENTS = 'https://events.openvibe.network';const GRANTS = ['media.object.upload', 'media.object.read', 'events.app.publish', 'events.app.read', 'codes.release.manage'];--const STEPS = ['account', 'discovery', 'project', 'app', 'grants', 'media', 'events', 'release', 'credentials', 'cleanup'];++const STEPS = ['account', 'discovery', 'project', 'app', 'grants', 'media', 'events', 'export', 'release', 'credentials', 'cleanup'];const STEP_TIMEOUT_MS = 120000;const withoutQuery = (u) => { try { const x = new URL(u); return `${x.origin}${x.pathname}${x.search ? ' (signed)' : ''}`; } catch { return u; } };@@@ -93,11 +98,11 @@ async function tryToken(fetchImpl, network, clientId, clientSecret, audience) {}/**-- * Runs the ten steps. account: { token } | { username, password, register }. onSecret(value) is++ * Runs the eleven steps. account: { token } | { username, password, register }. onSecret(value) is* called with every password, secret and token as soon as it is known, so the caller can mask it.* → { ok, steps: [{ name, ok, skipped?, error? }], projectId, appId }*/--async function runDeveloperPath({ network = PRODUCTION_NETWORK, codes = PRODUCTION_CODES, fetch: fetchImpl = globalThis.fetch, account, log = (m) => console.log(m), onSecret = () => {}, now = () => new Date() }) {++async function runDeveloperPath({ network = PRODUCTION_NETWORK, codes = PRODUCTION_CODES, media = PRODUCTION_MEDIA, events = PRODUCTION_EVENTS, fetch: fetchImpl = globalThis.fetch, account, log = (m) => console.log(m), onSecret = () => {}, now = () => new Date() }) {network = network.replace(/\/+$/, '');codes = codes.replace(/\/+$/, '');const say = (m) => log(` ${m}`);@@@ -206,6 +211,7 @@ async function runDeveloperPath({ network = PRODUCTION_NETWORK, codes = PRODUCTIawait step('events', async () => {const pub = await publishAppEvent(publishConfig(creds()), { name: 'developer_path.ran', subject: { type: 'check', id: 'developer-path' }, payload: { at: now().toISOString() } }, { fetch: fetchImpl });say(`published ${pub.event_type} ${pub.event_id} -> seq ${pub.seq}`);++ st.eventId = pub.event_id;const cursorPath = path.join(dir, 'cursor.json');createCursorStore(cursorPath).set('pull', Math.max(0, pub.seq - 1));const seen = [];@@@ -220,6 +226,35 @@ async function runDeveloperPath({ network = PRODUCTION_NETWORK, codes = PRODUCTIsay(`pulled ${topics[0]} with events.app.read: ${n} event(s), found ${mine.id} at seq ${mine.seq}`);});++ await step('export', async () => {++ const mint = async (audience) => {++ const res = await fetchImpl(`${network}/api/v1/projects/${st.projectId}/export-tokens`, {++ method: 'POST', headers: { Authorization: `Bearer ${st.token}`, 'Content-Type': 'application/json', Accept: 'application/json' },++ body: JSON.stringify({ audience, env: 'sandbox' }),++ });++ const b = await res.json().catch(() => ({}));++ if (res.status !== 201 || typeof b.access_token !== 'string') throw new Error(`export token for ${audience}: ${res.status} ${b.code || b.error || ''}`.trim());++ onSecret(b.access_token);++ return b.access_token;++ };++ const auth = (t) => ({ Authorization: `Bearer ${t}`, Accept: 'application/json' });++ const mt = await mint('openvibe.media');++ const list = await fetchImpl(`${media.replace(/\/+$/, '')}/api/v1/${st.projectId}/files?limit=10`, { headers: auth(mt) });++ if (list.status !== 200) throw new Error(`Media listed the project's objects with ${list.status}`);++ const listed = await list.json().catch(() => ({}));++ say(`Media lists the project's sandbox files with the export token (${(listed.files || listed.objects || []).length} now)`);++ const form = new FormData();++ form.append('file', new Blob(['x'], { type: 'text/plain' }), 'x.txt');++ const write = await fetchImpl(`${media.replace(/\/+$/, '')}/api/v1/${st.projectId}/files`, { method: 'POST', headers: auth(mt), body: form });++ if (write.status !== 403) throw new Error(`a write with the export token answered ${write.status}, not 403`);++ say('a write with it is refused (403)');++ const et = await mint('openvibe.events');++ const pull = await fetchImpl(`${events.replace(/\/+$/, '')}/api/v1/events?topic=${encodeURIComponent(`app.${projectKey(st.projectId)}.*`)}&after_seq=0&limit=100`, { headers: auth(et) });++ const got = pull.status === 200 ? ((await pull.json()).events || []) : [];++ if (!got.some((e) => (e.event || e).event_id === st.eventId)) throw new Error(`Events did not return the event published in step 7 with the export token (${pull.status})`);++ say(`Events returns the project's event ${st.eventId} with the other`);++ });++await step('release', async () => {const token = await appToken(fetchImpl, network, st.appId, st.secret, 'openvibe.codes');onSecret(token);@@@ -324,7 +359,7 @@ async function main(argv = process.argv.slice(2), env = process.env) {console.log(`OpenVibe developer path against ${network}\n`);const started = Date.now();const codes = (env.OV_CODES_URL || PRODUCTION_CODES).replace(/\/+$/, '');-- const result = await runDeveloperPath({ network, codes, account, onSecret });++ const result = await runDeveloperPath({ network, codes, media: env.OV_MEDIA_URL || PRODUCTION_MEDIA, events: env.OV_EVENTS_URL || PRODUCTION_EVENTS, account, onSecret });// --result <file>: a JSON record for the scheduled production run (OpenVibe.Host openvibe-devpath.timer,// shown on openvibe.network/status): step names and outcomes only — never an error text, URL, id or secret.const out = argv.indexOf('--result');
@@ -157,15 +157,16 @@ scoped credentials only, then rotates and revokes those credentials and cleans u
@@ -157,15 +157,16 @@ scoped credentials only, then rotates and revokes those credentials and cleans u
| 5. grants | requests `media.object.upload`, `media.object.read`, `events.app.publish`, `events.app.read`, `codes.release.manage`; all must be approved at once |
| 5. grants | requests `media.object.upload`, `media.object.read`, `events.app.publish`, `events.app.read`, `codes.release.manage`; all must be approved at once |
| 6. media | [media-uploader](examples/media-uploader): upload a small file, read it back, delete it |
| 6. media | [media-uploader](examples/media-uploader): upload a small file, read it back, delete it |
| 7. events | [event-subscriber](examples/event-subscriber): publish `app.<project_key>.developer_path.ran`, pull it with a cursor |
| 7. events | [event-subscriber](examples/event-subscriber): publish `app.<project_key>.developer_path.ran`, pull it with a cursor |
-| 8. release | as the app, on OpenVibe.Codes (`OV_CODES_URL`): a sandbox release of itself goes draft → published → deprecated → revoked, checked in the public list each time |
-| 9. credentials | rotates with no overlap and revokes the first credential; the old secret must be refused at `/oauth/token`, the new one must work |
-| 10. cleanup | archives the project, even when an earlier step failed; the app's secret must then be refused |
+| 8. export | as the project's owner: Network mints 5-minute read-only export tokens; Media lists the project's sandbox files with one and refuses an upload; Events returns the event from step 7 with the other |
+| 9. release | as the app, on OpenVibe.Codes (`OV_CODES_URL`): a sandbox release of itself goes draft → published → deprecated → revoked, checked in the public list each time |
+| 10. credentials | rotates with no overlap and revokes the first credential; the old secret must be refused at `/oauth/token`, the new one must work |
+| 11. cleanup | archives the project, even when an earlier step failed; the app's secret must then be refused |
**In CI**, on every push, `npm run developer-path:mock` runs the same code against
**In CI**, on every push, `npm run developer-path:mock` runs the same code against
`openvibe-sdk/testing`'s mock platform (the `developer-path` job; `test/developer-path.test.js`
`openvibe-sdk/testing`'s mock platform (the `developer-path` job; `test/developer-path.test.js`
also checks it leaves nothing behind, never prints a secret, and still cleans up after a failure).
also checks it leaves nothing behind, never prints a secret, and still cleans up after a failure).
The mock has no account API, so `scripts/developer-path-mock.js` adds register and login routes
The mock has no account API, so `scripts/developer-path-mock.js` adds register and login routes
-that mint mock user tokens, and stands in for Codes' release API; every other step is the platform mock's own.
+that mint mock user tokens, mints export tokens, and stands in for Codes' release API; every other step is the platform mock's own.
**Against production** it runs only when a person starts it. Credentials come from the
**Against production** it runs only when a person starts it. Credentials come from the
environment (or `./.env`) only:
environment (or `./.env`) only:
@@ -181,7 +182,7 @@ It refuses to start without credentials, and refuses in CI against the productio
@@ -181,7 +182,7 @@ It refuses to start without credentials, and refuses in CI against the productio
set and `OV_NETWORK_URL` unset or `https://openvibe.network`), so it can later run in CI against
set and `OV_NETWORK_URL` unset or `https://openvibe.network`), so it can later run in CI against
an integration environment but never against production by accident. It masks every password,
an integration environment but never against production by accident. It masks every password,
secret and token in its output and prints signed URLs without their signature. Exit `0` when all
secret and token in its output and prints signed URLs without their signature. Exit `0` when all
-ten steps passed, `1` when one failed, `2` when it refused to start.
+eleven steps passed, `1` when one failed, `2` when it refused to start.
It does not yet cover webhook delivery to an external endpoint (that needs a public https host)
It does not yet cover webhook delivery to an external endpoint (that needs a public https host)
or publishing an app release in OpenVibe.Codes.
or publishing an app release in OpenVibe.Codes.
@@ -9,7 +9,8 @@
@@ -9,7 +9,8 @@
* their public origins with real RS256 tokens. It has no account API, so this adds the two routes
* their public origins with real RS256 tokens. It has no account API, so this adds the two routes
* the flow uses, /api/auth/register and /api/auth/login, as a thin layer: register creates a mock
* the flow uses, /api/auth/register and /api/auth/login, as a thin layer: register creates a mock
* user and returns a Network user token for it; login returns one for an existing user (the mock
* user and returns a Network user token for it; login returns one for an existing user (the mock
- * does not check passwords). It also stands in for OpenVibe.Codes' release API (the release step):
+ * does not check passwords). It also mints Network's export tokens (the export step, signed by the
+ * platform like any app token) and stands in for OpenVibe.Codes' release API (the release step):
* an app token for openvibe.codes carrying codes.release.manage manages the app's own releases,
* an app token for openvibe.codes carrying codes.release.manage manages the app's own releases,
* draft → published → deprecated → revoked, and the public list shows every release but drafts, like Codes.
* draft → published → deprecated → revoked, and the public list shows every release but drafts, like Codes.
* Everything else is the platform mock's own.
* Everything else is the platform mock's own.
@@ -63,6 +64,15 @@ function withAccounts(platform, network = PRODUCTION_NETWORK) {
@@ -63,6 +64,15 @@ function withAccounts(platform, network = PRODUCTION_NETWORK) {
const user = byName.get(username.toLowerCase());
const user = byName.get(username.toLowerCase());
return user ? reply(200, { token: platform.signUserToken(user) }) : reply(401, { error: 'Invalid credentials' });
return user ? reply(200, { token: platform.signUserToken(user) }) : reply(401, { error: 'Invalid credentials' });
}
}
+ // Network's export tokens (WS-N task 9): read-only app-shaped tokens for the project's owner, as Network mints them.
+ const ex = url.origin === network && method === 'POST' && url.pathname.match(/^\/api\/v1\/projects\/(prj_[0-9A-Z]+)\/export-tokens$/i);
+ if (ex) {
+ const body = JSON.parse(init.body || '{}');
+ const caps = { 'openvibe.media': ['media.object.list', 'media.object.read'], 'openvibe.events': ['events.app.read'] }[body.audience];
+ if (!caps) return reply(422, { code: 'export.invalid_audience' });
+ const token = platform.signAppToken(`app_${ex[1].slice(4)}`, { audience: body.audience, capabilities: caps, projectId: ex[1], env: body.env || 'production' });
+ return reply(201, { access_token: token, token_type: 'Bearer', expires_in: 300, scope: caps.join(' ') });
+ }
if (url.origin === PRODUCTION_CODES) return codes(url, method, init);
if (url.origin === PRODUCTION_CODES) return codes(url, method, init);
return platform.fetch(input, init);
return platform.fetch(input, init);
};
};
@@ -13,13 +13,16 @@
@@ -13,13 +13,16 @@
* approved at once for the project owner inside the sandbox allowance
* approved at once for the project owner inside the sandbox allowance
* 6. media examples/media-uploader: upload a small file, read it back, delete it
* 6. media examples/media-uploader: upload a small file, read it back, delete it
* 7. events examples/event-subscriber: publish app.<project_key>.developer_path.ran, pull it
* 7. events examples/event-subscriber: publish app.<project_key>.developer_path.ran, pull it
- * 8. release as the app, on OpenVibe.Codes: a sandbox release of itself goes draft →
+ * 8. export as the project's owner: Network mints read-only export tokens (5 minutes); Media
+ * lists the project's sandbox files with one and refuses an upload; Events returns the
+ * event from step 7 with the other (roadmap WS-N task 9, the project export)
+ * 9. release as the app, on OpenVibe.Codes: a sandbox release of itself goes draft →
* published → deprecated → revoked, checked in the public list at each step (a
* published → deprecated → revoked, checked in the public list at each step (a
* draft is never listed; a revoked release stays listed, marked revoked)
* draft is never listed; a revoked release stays listed, marked revoked)
* (roadmap WS-N task 5, the app release flow in production)
* (roadmap WS-N task 5, the app release flow in production)
- * 9. credentials rotate with no overlap, revoke the first credential; the old secret is refused
+ * 10. credentials rotate with no overlap, revoke the first credential; the old secret is refused
* at /oauth/token and the new one works
* at /oauth/token and the new one works
- * 10. cleanup archive the project (always attempted once it exists); the app's secret is
+ * 11. cleanup archive the project (always attempted once it exists); the app's secret is
* refused afterwards
* refused afterwards
*
*
* The same flow runs in CI against openvibe-sdk/testing's mock platform (scripts/developer-path-mock.js,
* The same flow runs in CI against openvibe-sdk/testing's mock platform (scripts/developer-path-mock.js,
@@ -50,8 +53,10 @@ const { createSubscriber, createCursorStore, loadConfig: subscriberConfig } = re
@@ -50,8 +53,10 @@ const { createSubscriber, createCursorStore, loadConfig: subscriberConfig } = re
const PRODUCTION_NETWORK = 'https://openvibe.network';
const PRODUCTION_NETWORK = 'https://openvibe.network';
const PRODUCTION_CODES = 'https://openvibe.codes';
const PRODUCTION_CODES = 'https://openvibe.codes';
+const PRODUCTION_MEDIA = 'https://openvibe.media';
+const PRODUCTION_EVENTS = 'https://events.openvibe.network';
const GRANTS = ['media.object.upload', 'media.object.read', 'events.app.publish', 'events.app.read', 'codes.release.manage'];
const GRANTS = ['media.object.upload', 'media.object.read', 'events.app.publish', 'events.app.read', 'codes.release.manage'];
-const STEPS = ['account', 'discovery', 'project', 'app', 'grants', 'media', 'events', 'release', 'credentials', 'cleanup'];
+const STEPS = ['account', 'discovery', 'project', 'app', 'grants', 'media', 'events', 'export', 'release', 'credentials', 'cleanup'];
const STEP_TIMEOUT_MS = 120000;
const STEP_TIMEOUT_MS = 120000;
const withoutQuery = (u) => { try { const x = new URL(u); return `${x.origin}${x.pathname}${x.search ? ' (signed)' : ''}`; } catch { return u; } };
const withoutQuery = (u) => { try { const x = new URL(u); return `${x.origin}${x.pathname}${x.search ? ' (signed)' : ''}`; } catch { return u; } };
@@ -93,11 +98,11 @@ async function tryToken(fetchImpl, network, clientId, clientSecret, audience) {
@@ -93,11 +98,11 @@ async function tryToken(fetchImpl, network, clientId, clientSecret, audience) {
}
}
/**
/**
- * Runs the ten steps. account: { token } | { username, password, register }. onSecret(value) is
+ * Runs the eleven steps. account: { token } | { username, password, register }. onSecret(value) is
* called with every password, secret and token as soon as it is known, so the caller can mask it.
* called with every password, secret and token as soon as it is known, so the caller can mask it.
* → { ok, steps: [{ name, ok, skipped?, error? }], projectId, appId }
* → { ok, steps: [{ name, ok, skipped?, error? }], projectId, appId }
*/
*/
-async function runDeveloperPath({ network = PRODUCTION_NETWORK, codes = PRODUCTION_CODES, fetch: fetchImpl = globalThis.fetch, account, log = (m) => console.log(m), onSecret = () => {}, now = () => new Date() }) {
+async function runDeveloperPath({ network = PRODUCTION_NETWORK, codes = PRODUCTION_CODES, media = PRODUCTION_MEDIA, events = PRODUCTION_EVENTS, fetch: fetchImpl = globalThis.fetch, account, log = (m) => console.log(m), onSecret = () => {}, now = () => new Date() }) {
network = network.replace(/\/+$/, '');
network = network.replace(/\/+$/, '');
codes = codes.replace(/\/+$/, '');
codes = codes.replace(/\/+$/, '');
const say = (m) => log(` ${m}`);
const say = (m) => log(` ${m}`);
@@ -206,6 +211,7 @@ async function runDeveloperPath({ network = PRODUCTION_NETWORK, codes = PRODUCTI
@@ -206,6 +211,7 @@ async function runDeveloperPath({ network = PRODUCTION_NETWORK, codes = PRODUCTI
await step('events', async () => {
await step('events', async () => {
const pub = await publishAppEvent(publishConfig(creds()), { name: 'developer_path.ran', subject: { type: 'check', id: 'developer-path' }, payload: { at: now().toISOString() } }, { fetch: fetchImpl });
const pub = await publishAppEvent(publishConfig(creds()), { name: 'developer_path.ran', subject: { type: 'check', id: 'developer-path' }, payload: { at: now().toISOString() } }, { fetch: fetchImpl });
say(`published ${pub.event_type} ${pub.event_id} -> seq ${pub.seq}`);
say(`published ${pub.event_type} ${pub.event_id} -> seq ${pub.seq}`);
+ st.eventId = pub.event_id;
const cursorPath = path.join(dir, 'cursor.json');
const cursorPath = path.join(dir, 'cursor.json');
createCursorStore(cursorPath).set('pull', Math.max(0, pub.seq - 1));
createCursorStore(cursorPath).set('pull', Math.max(0, pub.seq - 1));
const seen = [];
const seen = [];
@@ -220,6 +226,35 @@ async function runDeveloperPath({ network = PRODUCTION_NETWORK, codes = PRODUCTI
@@ -220,6 +226,35 @@ async function runDeveloperPath({ network = PRODUCTION_NETWORK, codes = PRODUCTI
say(`pulled ${topics[0]} with events.app.read: ${n} event(s), found ${mine.id} at seq ${mine.seq}`);
say(`pulled ${topics[0]} with events.app.read: ${n} event(s), found ${mine.id} at seq ${mine.seq}`);
});
});
+ await step('export', async () => {
+ const mint = async (audience) => {
+ const res = await fetchImpl(`${network}/api/v1/projects/${st.projectId}/export-tokens`, {
+ method: 'POST', headers: { Authorization: `Bearer ${st.token}`, 'Content-Type': 'application/json', Accept: 'application/json' },
+ body: JSON.stringify({ audience, env: 'sandbox' }),
+ });
+ const b = await res.json().catch(() => ({}));
+ if (res.status !== 201 || typeof b.access_token !== 'string') throw new Error(`export token for ${audience}: ${res.status} ${b.code || b.error || ''}`.trim());
+ onSecret(b.access_token);
+ return b.access_token;
+ };
+ const auth = (t) => ({ Authorization: `Bearer ${t}`, Accept: 'application/json' });
+ const mt = await mint('openvibe.media');
+ const list = await fetchImpl(`${media.replace(/\/+$/, '')}/api/v1/${st.projectId}/files?limit=10`, { headers: auth(mt) });
+ if (list.status !== 200) throw new Error(`Media listed the project's objects with ${list.status}`);
+ const listed = await list.json().catch(() => ({}));
+ say(`Media lists the project's sandbox files with the export token (${(listed.files || listed.objects || []).length} now)`);
+ const form = new FormData();
+ form.append('file', new Blob(['x'], { type: 'text/plain' }), 'x.txt');
+ const write = await fetchImpl(`${media.replace(/\/+$/, '')}/api/v1/${st.projectId}/files`, { method: 'POST', headers: auth(mt), body: form });
+ if (write.status !== 403) throw new Error(`a write with the export token answered ${write.status}, not 403`);
+ say('a write with it is refused (403)');
+ const et = await mint('openvibe.events');
+ const pull = await fetchImpl(`${events.replace(/\/+$/, '')}/api/v1/events?topic=${encodeURIComponent(`app.${projectKey(st.projectId)}.*`)}&after_seq=0&limit=100`, { headers: auth(et) });
+ const got = pull.status === 200 ? ((await pull.json()).events || []) : [];
+ if (!got.some((e) => (e.event || e).event_id === st.eventId)) throw new Error(`Events did not return the event published in step 7 with the export token (${pull.status})`);
+ say(`Events returns the project's event ${st.eventId} with the other`);
+ });
+
await step('release', async () => {
await step('release', async () => {
const token = await appToken(fetchImpl, network, st.appId, st.secret, 'openvibe.codes');
const token = await appToken(fetchImpl, network, st.appId, st.secret, 'openvibe.codes');
onSecret(token);
onSecret(token);
@@ -324,7 +359,7 @@ async function main(argv = process.argv.slice(2), env = process.env) {
@@ -324,7 +359,7 @@ async function main(argv = process.argv.slice(2), env = process.env) {
console.log(`OpenVibe developer path against ${network}\n`);
console.log(`OpenVibe developer path against ${network}\n`);
const started = Date.now();
const started = Date.now();
const codes = (env.OV_CODES_URL || PRODUCTION_CODES).replace(/\/+$/, '');
const codes = (env.OV_CODES_URL || PRODUCTION_CODES).replace(/\/+$/, '');
- const result = await runDeveloperPath({ network, codes, account, onSecret });
+ const result = await runDeveloperPath({ network, codes, media: env.OV_MEDIA_URL || PRODUCTION_MEDIA, events: env.OV_EVENTS_URL || PRODUCTION_EVENTS, account, onSecret });
// --result <file>: a JSON record for the scheduled production run (OpenVibe.Host openvibe-devpath.timer,
// --result <file>: a JSON record for the scheduled production run (OpenVibe.Host openvibe-devpath.timer,
// shown on openvibe.network/status): step names and outcomes only — never an error text, URL, id or secret.
// shown on openvibe.network/status): step names and outcomes only — never an error text, URL, id or secret.
const out = argv.indexOf('--result');
const out = argv.indexOf('--result');