akari-video 0.1.76 → 0.1.77
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/package.json +1 -1
- package/src/service-urls.cjs +38 -0
- package/src/service-urls.d.cts +9 -0
- package/src/store-device-connect.mjs +7 -6
- package/vendor/docs/contract-2026-07-13-asset-library.md +1 -1
- package/vendor/docs/contract-2026-09-02-asset-reference-model.md +4 -3
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/asset-resolver/README.md +4 -4
- package/vendor/packages/asset-resolver/bin/akari-assets.mjs +3 -2
- package/vendor/packages/asset-resolver/package.json +1 -1
- package/vendor/packages/asset-resolver/src/env.mjs +5 -5
- package/vendor/packages/asset-resolver/src/library.mjs +4 -1
- package/vendor/packages/asset-resolver/src/service-urls.mjs +14 -0
- package/vendor/packages/asset-resolver/src/shell-reference.mjs +92 -0
- package/vendor/packages/asset-resolver/test/local-library.test.mjs +10 -5
- package/vendor/packages/asset-resolver/test/service-urls.test.mjs +100 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.77",
|
|
4
4
|
"description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Shared by the launcher, resolver and shell. Keep this module browser-safe;
|
|
2
|
+
// CommonJS also lets the shell's compiled CommonJS modules load it in Node.
|
|
3
|
+
const AKARI_HOST = 'akari.video';
|
|
4
|
+
const LEGACY_AKARI_HOST = 'akari-oss.app';
|
|
5
|
+
const DEFAULT_STORE_API = `https://${AKARI_HOST}`;
|
|
6
|
+
const DEFAULT_STORE_BASE_URL = `${DEFAULT_STORE_API}/api/store`;
|
|
7
|
+
const DEFAULT_STORE_LAB_BASE_URL = `${DEFAULT_STORE_API}/lab`;
|
|
8
|
+
const DEFAULT_ASSETS_BASE_URL = `${DEFAULT_STORE_API}/assets/`;
|
|
9
|
+
const DEFAULT_CATALOG_URL = `${DEFAULT_ASSETS_BASE_URL}catalog.json`;
|
|
10
|
+
|
|
11
|
+
/** Migrate only the former official origin; preserve custom servers and paths. */
|
|
12
|
+
function normalizeAkariUrl(value) {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(value);
|
|
15
|
+
if ((url.protocol === 'https:' || url.protocol === 'http:') && url.hostname === LEGACY_AKARI_HOST) {
|
|
16
|
+
url.protocol = 'https:';
|
|
17
|
+
url.hostname = AKARI_HOST;
|
|
18
|
+
return url.href;
|
|
19
|
+
}
|
|
20
|
+
} catch { /* Local paths and malformed values keep their existing handling. */ }
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Store credentials contain .../api/store; purchase pages live under .../lab. */
|
|
25
|
+
function deriveStoreLabBaseUrl(storeApiUrl) {
|
|
26
|
+
if (!storeApiUrl) return DEFAULT_STORE_LAB_BASE_URL;
|
|
27
|
+
return normalizeAkariUrl(storeApiUrl).replace(/\/api\/store\/?$/, '/lab');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
exports.AKARI_HOST = AKARI_HOST;
|
|
31
|
+
exports.LEGACY_AKARI_HOST = LEGACY_AKARI_HOST;
|
|
32
|
+
exports.DEFAULT_STORE_API = DEFAULT_STORE_API;
|
|
33
|
+
exports.DEFAULT_STORE_BASE_URL = DEFAULT_STORE_BASE_URL;
|
|
34
|
+
exports.DEFAULT_STORE_LAB_BASE_URL = DEFAULT_STORE_LAB_BASE_URL;
|
|
35
|
+
exports.DEFAULT_ASSETS_BASE_URL = DEFAULT_ASSETS_BASE_URL;
|
|
36
|
+
exports.DEFAULT_CATALOG_URL = DEFAULT_CATALOG_URL;
|
|
37
|
+
exports.normalizeAkariUrl = normalizeAkariUrl;
|
|
38
|
+
exports.deriveStoreLabBaseUrl = deriveStoreLabBaseUrl;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const AKARI_HOST: string;
|
|
2
|
+
export const LEGACY_AKARI_HOST: string;
|
|
3
|
+
export const DEFAULT_STORE_API: string;
|
|
4
|
+
export const DEFAULT_STORE_BASE_URL: string;
|
|
5
|
+
export const DEFAULT_STORE_LAB_BASE_URL: string;
|
|
6
|
+
export const DEFAULT_ASSETS_BASE_URL: string;
|
|
7
|
+
export const DEFAULT_CATALOG_URL: string;
|
|
8
|
+
export function normalizeAkariUrl(value: string): string;
|
|
9
|
+
export function deriveStoreLabBaseUrl(storeApiUrl: string | undefined): string;
|
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
import { homedir, hostname } from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import { DEFAULT_STORE_BASE_URL, normalizeAkariUrl } from './service-urls.cjs';
|
|
9
|
+
export { DEFAULT_STORE_BASE_URL } from './service-urls.cjs';
|
|
9
10
|
const CREDENTIALS_FILE = 'store-credentials.json';
|
|
10
11
|
|
|
11
12
|
export function resolveAkariHome(env = process.env) {
|
|
@@ -22,7 +23,7 @@ export function readCredentials(env = process.env) {
|
|
|
22
23
|
try {
|
|
23
24
|
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
24
25
|
if (typeof parsed?.token !== 'string' || typeof parsed?.url !== 'string') return null;
|
|
25
|
-
return parsed;
|
|
26
|
+
return { ...parsed, url: normalizeAkariUrl(parsed.url) };
|
|
26
27
|
} catch {
|
|
27
28
|
return null;
|
|
28
29
|
}
|
|
@@ -57,7 +58,7 @@ export function defaultOpenBrowser(url, platform = process.platform) {
|
|
|
57
58
|
export async function fetchStoreEntitlements(fetchImpl, baseUrl, token) {
|
|
58
59
|
let response;
|
|
59
60
|
try {
|
|
60
|
-
response = await fetchImpl(`${baseUrl}/v1/entitlements`, {
|
|
61
|
+
response = await fetchImpl(`${normalizeAkariUrl(baseUrl).replace(/\/+$/, '')}/v1/entitlements`, {
|
|
61
62
|
headers: { authorization: `Bearer ${token}` }
|
|
62
63
|
});
|
|
63
64
|
} catch (error) {
|
|
@@ -95,7 +96,7 @@ export async function validateAndSaveCredentials(
|
|
|
95
96
|
return { status: 'error', error };
|
|
96
97
|
}
|
|
97
98
|
const credentials = {
|
|
98
|
-
url: baseUrl,
|
|
99
|
+
url: normalizeAkariUrl(baseUrl),
|
|
99
100
|
token,
|
|
100
101
|
email: data.email,
|
|
101
102
|
connected_at: now().toISOString()
|
|
@@ -113,7 +114,7 @@ export async function startDeviceConnection({
|
|
|
113
114
|
label = `AKARI Video (${hostname()})`,
|
|
114
115
|
openBrowser
|
|
115
116
|
} = {}) {
|
|
116
|
-
const normalizedBaseUrl = baseUrl.replace(
|
|
117
|
+
const normalizedBaseUrl = normalizeAkariUrl(baseUrl).replace(/\/+$/, '');
|
|
117
118
|
let response;
|
|
118
119
|
try {
|
|
119
120
|
response = await fetchImpl(`${normalizedBaseUrl}/device/start`, {
|
|
@@ -159,7 +160,7 @@ export async function pollDeviceConnection({
|
|
|
159
160
|
}) {
|
|
160
161
|
let response;
|
|
161
162
|
try {
|
|
162
|
-
response = await fetchImpl(`${baseUrl}/device/claim`, {
|
|
163
|
+
response = await fetchImpl(`${normalizeAkariUrl(baseUrl).replace(/\/+$/, '')}/device/claim`, {
|
|
163
164
|
method: 'POST',
|
|
164
165
|
headers: { 'content-type': 'application/json' },
|
|
165
166
|
body: JSON.stringify({ deviceCode })
|
|
@@ -442,7 +442,7 @@ catalog に載せる素材は、取得元のライセンスが CC0 相当(帰
|
|
|
442
442
|
出どころはカタログ掲載(lab)、明示 origin タグ(site / own)、AKARI 配布元の source.url(lab)、
|
|
443
443
|
その他の source.url あり(site)、それ以外(own)の順で決める。
|
|
444
444
|
AKARI 配布元は URL を解析し、ホスト github.com かつパスの最初のセグメントが AkariLabs、
|
|
445
|
-
またはホスト akari
|
|
445
|
+
またはホスト akari.video とそのサブドメインで判定する。移行期間中は旧公式ホストとそのサブドメインも受け入れる。壊れた URL は site とする。
|
|
446
446
|
素材ディレクトリの `CREDIT.txt` はクレジット文面 1 行。文面がある場合は
|
|
447
447
|
`license.attribution_required: true`、source がある場合はその attribution_required も true にする。
|
|
448
448
|
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
`akari-assets list`(または `akari assets list`)の先頭行で実際の置き場を確認する。
|
|
5
5
|
以下の `<ライブラリの置き場>` はその表示先を指し、音源はその下の `audio/` に入る。
|
|
6
6
|
|
|
7
|
-
- 状態:
|
|
7
|
+
- 状態: 実装済み(機械層 2026-09-02。シェル UI の採用 = 取り込みフローの reference 既定化・プロジェクト面の「参照」札・
|
|
8
|
+
プレビュー / タイムラインの経路・「素材をまとめる」は 2026-09-22 `task/2026-09-21-library-reference-in-shell` で採用)
|
|
8
9
|
- 決定日: 2026-09-02
|
|
9
10
|
- 実装: `packages/asset-resolver`(記帳・実体化)/ `packages/render-cut`・`packages/edit-lint`(解決)
|
|
10
11
|
|
|
@@ -99,8 +100,8 @@ render-cut は子プロセスの起動前に排他的に作成し、OSR / GPU(
|
|
|
99
100
|
|
|
100
101
|
## 5. スコープ外(後続)
|
|
101
102
|
|
|
102
|
-
-
|
|
103
|
-
|
|
103
|
+
- ~~シェル UI の採用(取り込みフローの reference 既定化・プロジェクト面の「参照」バッジ・
|
|
104
|
+
プレビュー経路のフォールバック)~~ → 2026-09-22 に採用済み(「状態」行を参照)。残るのは置き場側の変化で札を自動更新する件
|
|
104
105
|
- 共有キャッシュの容量管理 UI
|
|
105
106
|
|
|
106
107
|
出自: 2026-09-02 の素材パネル再設計ラウンドの裁定 3(実体 = 共有キャッシュ・プロジェクトには参照・
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.77",
|
|
4
4
|
"description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。 [akari-video npm vendor: bin/akari.mjs is reference-only. These CLI entrypoints are not included in the akari-video npm package. Use `akari doctor --json` and run the path reported in `render_cut.path`. Full installations provide it in a monorepo checkout, ~/.akari/app, /Applications/AKARI Video.app/Contents/Resources/packages, or %LOCALAPPDATA%\\Programs\\@akari-videoshell\\resources\\packages.]",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -78,7 +78,7 @@ checksums 不一致は、いずれも `AssetResolverError`(`code: 'download_fa
|
|
|
78
78
|
{
|
|
79
79
|
"schema": "akari-assets-catalog/v0",
|
|
80
80
|
"version": "2026-08-04",
|
|
81
|
-
"base": "https://akari
|
|
81
|
+
"base": "https://akari.video/assets/",
|
|
82
82
|
"items": [
|
|
83
83
|
{
|
|
84
84
|
"id": "br-typing-laptop",
|
|
@@ -110,9 +110,9 @@ checksums 不一致は、いずれも `AssetResolverError`(`code: 'download_fa
|
|
|
110
110
|
| --- | --- | --- |
|
|
111
111
|
| `AKARI_HOME` | `~/.akari` | マシン状態(library-location.json・カタログキャッシュ・store-credentials.json)。作業場なしでは旧 assets/ もここに置く |
|
|
112
112
|
| `AKARI_LIBRARY_ROOT` | 未設定 | ライブラリの書き込み先の明示上書き(読みは旧置き場にもフォールバック) |
|
|
113
|
-
| `AKARI_ASSETS_CATALOG` | `https://akari
|
|
113
|
+
| `AKARI_ASSETS_CATALOG` | `https://akari.video/assets/catalog.json` | カタログの取得元。**URL** ならリモート fetch、それ以外はローカルファイルパスとして読む(未デプロイの開発時は store リポのローカル出力を指す) |
|
|
114
114
|
| `AKARI_ASSETS_BASE` | カタログの `base` フィールド | 素材実体の配信ベースの上書き(ローカル開発でディレクトリを直接指すときに使う) |
|
|
115
|
-
| `AKARI_STORE_API` | `https://akari
|
|
115
|
+
| `AKARI_STORE_API` | `https://akari.video` | entitlements API のホスト上書き。未設定時は `~/.akari/store-credentials.json` の `url`(`akari store connect` が書き込む値)から組み立てる |
|
|
116
116
|
|
|
117
117
|
`store-credentials.json` が無い場合、または entitlements API への到達に失敗した場合は
|
|
118
118
|
「entitlements 不明」として無料素材のみが使える状態にフォールバックする(黙って有料を通したり、
|
|
@@ -193,7 +193,7 @@ root(絶対パス)、state(pending / migrating / done / declined)、deci
|
|
|
193
193
|
| `preview` / `mediaFile` | ローカル素材では `preview.png` / 直下で一意な主メディアのファイル名。無ければ null |
|
|
194
194
|
|
|
195
195
|
AKARI 配布元は URL を解析し、ホスト `github.com` かつパスの最初のセグメントが `AkariLabs`、
|
|
196
|
-
またはホスト `akari
|
|
196
|
+
またはホスト `akari.video` とそのサブドメインで判定する。移行期間中は旧公式ホストとそのサブドメインも受け入れる。壊れた URL は `site` とする。
|
|
197
197
|
|
|
198
198
|
主メディアはシェルと同じ一意解決の規則で、複数テイクから勝手に選ばない。
|
|
199
199
|
still の `preview.png` は主メディア候補から除く。音・映像・画像に加え、取り込み対象の
|
|
@@ -16,6 +16,7 @@ import { startBrowseServer } from '../src/browse-server.mjs';
|
|
|
16
16
|
import { bundleProjectReferences } from '../src/bundle.mjs';
|
|
17
17
|
import { cacheCatalog, loadCatalog } from '../src/catalog.mjs';
|
|
18
18
|
import { resolve as resolveAsset } from '../src/resolve.mjs';
|
|
19
|
+
import { DEFAULT_CATALOG_URL, DEFAULT_STORE_API } from '../src/service-urls.mjs';
|
|
19
20
|
import { composeState } from '../src/state.mjs';
|
|
20
21
|
|
|
21
22
|
function flagValue(args, name) {
|
|
@@ -193,9 +194,9 @@ function printUsage() {
|
|
|
193
194
|
環境変数:
|
|
194
195
|
AKARI_HOME マシン設定の置き場(既定: ~/.akari)
|
|
195
196
|
AKARI_LIBRARY_ROOT ライブラリの置き場の上書き
|
|
196
|
-
AKARI_ASSETS_CATALOG カタログの取得元。URL またはローカルパス(既定:
|
|
197
|
+
AKARI_ASSETS_CATALOG カタログの取得元。URL またはローカルパス(既定: ${DEFAULT_CATALOG_URL})
|
|
197
198
|
AKARI_ASSETS_BASE 素材実体の配信ベースの上書き(既定はカタログの "base" フィールド)
|
|
198
|
-
AKARI_STORE_API entitlements API のホスト上書き(既定:
|
|
199
|
+
AKARI_STORE_API entitlements API のホスト上書き(既定: ${DEFAULT_STORE_API})`);
|
|
199
200
|
}
|
|
200
201
|
|
|
201
202
|
async function main() {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"version": "0.0.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "無料素材の参照配布 + オンデマンド取得 resolver。akari
|
|
6
|
+
"description": "無料素材の参照配布 + オンデマンド取得 resolver。akari.video/assets のカタログとエンタイトルメントを合成し、使った素材だけを ~/.akari/assets/ へ取得・sha256 検証・validate-asset 検証してから登録する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。",
|
|
7
7
|
"bin": {
|
|
8
8
|
"akari-assets": "bin/akari-assets.mjs"
|
|
9
9
|
},
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
export
|
|
11
|
+
import { DEFAULT_CATALOG_URL, DEFAULT_STORE_API, normalizeAkariUrl } from './service-urls.mjs';
|
|
12
|
+
export { DEFAULT_CATALOG_URL, DEFAULT_STORE_API } from './service-urls.mjs';
|
|
13
13
|
const CREDENTIALS_FILE = 'store-credentials.json';
|
|
14
14
|
const CATALOG_CACHE_FILE = 'catalog-cache.json';
|
|
15
15
|
|
|
@@ -30,7 +30,7 @@ export function resolveAkariHome(env = process.env) {
|
|
|
30
30
|
export function resolveCatalogSource(env = process.env) {
|
|
31
31
|
const raw = env.AKARI_ASSETS_CATALOG || DEFAULT_CATALOG_URL;
|
|
32
32
|
if (isRemoteLocation(raw)) {
|
|
33
|
-
return { kind: 'url', value: raw };
|
|
33
|
+
return { kind: 'url', value: normalizeAkariUrl(raw) };
|
|
34
34
|
}
|
|
35
35
|
return { kind: 'file', value: path.resolve(raw) };
|
|
36
36
|
}
|
|
@@ -45,7 +45,7 @@ export function resolveEffectiveBase(env = process.env, catalog) {
|
|
|
45
45
|
if (!base) {
|
|
46
46
|
throw new Error('素材の配信ベースが決まりません(catalog.base 未設定・AKARI_ASSETS_BASE 未設定)');
|
|
47
47
|
}
|
|
48
|
-
return base;
|
|
48
|
+
return normalizeAkariUrl(base);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export function resolveCredentialsPath(env = process.env) {
|
|
@@ -57,7 +57,7 @@ export function catalogCachePath(env = process.env) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function trimTrailingSlash(value) {
|
|
60
|
-
return value.replace(/\/+$/, '');
|
|
60
|
+
return normalizeAkariUrl(value).replace(/\/+$/, '');
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
@@ -3,10 +3,13 @@ import { readdirSync, statSync, readFileSync } from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { resolveAssetLibraryRoots } from '../../creator-root/src/index.mjs';
|
|
5
5
|
|
|
6
|
+
import { AKARI_HOST, LEGACY_AKARI_HOST } from './service-urls.mjs';
|
|
7
|
+
|
|
6
8
|
export const ASSET_CATEGORIES = ['overlay', 'still', 'scene3d', 'audio', 'broll', 'font'];
|
|
7
9
|
const FIRST_PARTY_SOURCES = [
|
|
8
10
|
{ hostname: 'github.com', firstPathSegment: 'AkariLabs' },
|
|
9
|
-
{ hostname:
|
|
11
|
+
{ hostname: AKARI_HOST, includeSubdomains: true },
|
|
12
|
+
{ hostname: LEGACY_AKARI_HOST, includeSubdomains: true },
|
|
10
13
|
];
|
|
11
14
|
|
|
12
15
|
function isFirstPartySource(sourceUrl) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
// The resolver is shipped under launcher/vendor/packages/asset-resolver.
|
|
6
|
+
// Both layouts point to the launcher's single canonical, browser-safe module;
|
|
7
|
+
// no duplicate constants or changes to the vendor manifest are needed.
|
|
8
|
+
const checkout = new URL('../../akari-launcher/src/service-urls.cjs', import.meta.url);
|
|
9
|
+
const packaged = new URL('../../../../src/service-urls.cjs', import.meta.url);
|
|
10
|
+
export const {
|
|
11
|
+
AKARI_HOST, LEGACY_AKARI_HOST, DEFAULT_STORE_API, DEFAULT_STORE_BASE_URL,
|
|
12
|
+
DEFAULT_STORE_LAB_BASE_URL, DEFAULT_ASSETS_BASE_URL, DEFAULT_CATALOG_URL,
|
|
13
|
+
normalizeAkariUrl, deriveStoreLabBaseUrl,
|
|
14
|
+
} = createRequire(import.meta.url)(fileURLToPath(existsSync(checkout) ? checkout : packaged));
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Thin shell entry point: keep the ledger and containment rules in project-references.
|
|
2
|
+
import { lstat, readdir, realpath, stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { resolveAssetLibraryRoots } from '../../creator-root/src/index.mjs';
|
|
6
|
+
import { readProjectReferences, resolveLibraryFallback } from './project-references.mjs';
|
|
7
|
+
|
|
8
|
+
const within = (root, target) => {
|
|
9
|
+
const rel = path.relative(root, target);
|
|
10
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel));
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function resolveProjectAssetPath(project, declaredPath, env = process.env) {
|
|
14
|
+
const normalized = declaredPath.replaceAll('\\', '/');
|
|
15
|
+
if (path.isAbsolute(normalized) || normalized.split('/').some(part => part === '..' || part === '.')) {
|
|
16
|
+
throw new Error('素材パスがプロジェクトの外を指しています');
|
|
17
|
+
}
|
|
18
|
+
const root = await realpath(project);
|
|
19
|
+
const local = path.resolve(root, normalized);
|
|
20
|
+
if (!within(root, local)) throw new Error('素材パスがプロジェクトの外を指しています');
|
|
21
|
+
// An existing local entry always wins, including a broken or escaping symlink:
|
|
22
|
+
// never silently replace an invalid project entry with a library file.
|
|
23
|
+
let exists = false;
|
|
24
|
+
try { await lstat(local); exists = true; } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
25
|
+
let ancestor = path.dirname(local);
|
|
26
|
+
while (within(root, ancestor)) {
|
|
27
|
+
try {
|
|
28
|
+
if (!within(root, await realpath(ancestor))) throw new Error('素材パスがプロジェクトの外を指しています');
|
|
29
|
+
break;
|
|
30
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
31
|
+
ancestor = path.dirname(ancestor);
|
|
32
|
+
}
|
|
33
|
+
if (exists) {
|
|
34
|
+
const actual = await realpath(local);
|
|
35
|
+
if (!within(root, actual)) throw new Error('素材パスがプロジェクトの外を指しています');
|
|
36
|
+
return (await stat(actual)).isFile() ? actual : null;
|
|
37
|
+
}
|
|
38
|
+
const references = await readProjectReferences(root);
|
|
39
|
+
for (const akariAssetsDir of resolveAssetLibraryRoots(env).read) {
|
|
40
|
+
const found = resolveLibraryFallback({ declaredPath: normalized, references, akariAssetsDir });
|
|
41
|
+
if (found) return found;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Enumerate only ledger-authorized files, in library read order; no symlink traversal. */
|
|
47
|
+
export async function listProjectReferenceAssets(project, env = process.env) {
|
|
48
|
+
const references = await readProjectReferences(project);
|
|
49
|
+
return Promise.all(references.map(async reference => {
|
|
50
|
+
const files = new Map();
|
|
51
|
+
let libraryDir;
|
|
52
|
+
if ([reference.category, reference.id].some(value => !value || value === '.' || value === '..' || /[\\/]/.test(value))) {
|
|
53
|
+
return { ...reference, files: [] };
|
|
54
|
+
}
|
|
55
|
+
for (const root of resolveAssetLibraryRoots(env).read) {
|
|
56
|
+
const directory = path.resolve(root, reference.category, reference.id);
|
|
57
|
+
if (!within(path.resolve(root), directory)) continue;
|
|
58
|
+
try { if (!within(await realpath(root), await realpath(directory))) continue; } catch { continue; }
|
|
59
|
+
const walk = async (dir, prefix = '') => {
|
|
60
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
61
|
+
const name = prefix + entry.name;
|
|
62
|
+
if (entry.isDirectory()) await walk(path.join(dir, entry.name), `${name}/`);
|
|
63
|
+
else {
|
|
64
|
+
const absolute = resolveLibraryFallback({
|
|
65
|
+
declaredPath: `assets/${reference.category}/${reference.id}/${name}`, references, akariAssetsDir: root,
|
|
66
|
+
});
|
|
67
|
+
if (absolute && !files.has(name)) {
|
|
68
|
+
libraryDir ??= directory;
|
|
69
|
+
files.set(name, { name, path: absolute, bytes: (await stat(absolute)).size });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
try { await walk(directory); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
75
|
+
}
|
|
76
|
+
return { ...reference, libraryDir, files: [...files.values()] };
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Resolve before synchronous timeline rendering, including audio and thumbnail consumers. */
|
|
81
|
+
export async function projectReferenceMediaUris(project, env = process.env, declaredPaths = []) {
|
|
82
|
+
const uris = {};
|
|
83
|
+
const paths = new Set(declaredPaths);
|
|
84
|
+
for (const asset of await listProjectReferenceAssets(project, env)) {
|
|
85
|
+
for (const file of asset.files) paths.add(`assets/${asset.category}/${asset.id}/${file.name}`);
|
|
86
|
+
}
|
|
87
|
+
for (const declared of paths) {
|
|
88
|
+
const actual = await resolveProjectAssetPath(project, declared, env);
|
|
89
|
+
if (actual) uris[declared.replaceAll('\\', '/')] = pathToFileURL(actual).href;
|
|
90
|
+
}
|
|
91
|
+
return uris;
|
|
92
|
+
}
|
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import test from 'node:test';
|
|
6
6
|
import { composeState } from '../src/state.mjs';
|
|
7
7
|
import { primaryMediaFile, sourceFields } from '../src/library.mjs';
|
|
8
|
+
import { LEGACY_AKARI_HOST } from '../src/service-urls.mjs';
|
|
8
9
|
import { setupFixtureEnv } from './helpers.mjs';
|
|
9
10
|
|
|
10
11
|
function fixture(t) {
|
|
@@ -110,12 +111,16 @@ for (const kind of ['bgm', 'jingle', 'sfx']) {
|
|
|
110
111
|
for (const [url, expected] of [
|
|
111
112
|
['https://github.com/AkariLabs-evil/x', 'site'],
|
|
112
113
|
['https://evil.example/github.com/AkariLabs/x', 'site'],
|
|
113
|
-
[
|
|
114
|
-
['https://
|
|
114
|
+
[`https://${LEGACY_AKARI_HOST}.evil.example/x`, 'site'],
|
|
115
|
+
['https://akari.video.evil.example/x', 'site'],
|
|
116
|
+
['https://evilakari.video/x', 'site'],
|
|
117
|
+
['https://akari.video/x', 'lab'],
|
|
118
|
+
['https://x.akari.video/x', 'lab'],
|
|
119
|
+
[`https://evil${LEGACY_AKARI_HOST}/x`, 'site'],
|
|
115
120
|
['https://github.com/other/AkariLabs/x', 'site'],
|
|
116
|
-
[
|
|
117
|
-
[
|
|
118
|
-
[
|
|
121
|
+
[`https://${LEGACY_AKARI_HOST}/x`, 'lab'],
|
|
122
|
+
[`https://assets.${LEGACY_AKARI_HOST}/x`, 'lab'],
|
|
123
|
+
[`https://cdn.assets.${LEGACY_AKARI_HOST}/x`, 'lab'],
|
|
119
124
|
['https://[broken', 'site'],
|
|
120
125
|
['not a URL', 'site'],
|
|
121
126
|
]) {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
|
+
import test from 'node:test';
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_ASSETS_BASE_URL, DEFAULT_CATALOG_URL, DEFAULT_STORE_API,
|
|
10
|
+
DEFAULT_STORE_BASE_URL, DEFAULT_STORE_LAB_BASE_URL, LEGACY_AKARI_HOST,
|
|
11
|
+
deriveStoreLabBaseUrl, normalizeAkariUrl,
|
|
12
|
+
} from '../src/service-urls.mjs';
|
|
13
|
+
import { resolveCatalogSource, resolveDownloadUrl, resolveEffectiveBase, resolveEntitlementsUrl } from '../src/env.mjs';
|
|
14
|
+
import { fetchEntitlements } from '../src/entitlements.mjs';
|
|
15
|
+
|
|
16
|
+
const repo = fileURLToPath(new URL('../../../', import.meta.url));
|
|
17
|
+
|
|
18
|
+
test('tracked sources and documentation cannot reintroduce the retired domain', () => {
|
|
19
|
+
const allowed = new Set([
|
|
20
|
+
'packages/akari-launcher/src/service-urls.cjs',
|
|
21
|
+
'packages/asset-resolver/test/service-urls.test.mjs',
|
|
22
|
+
]);
|
|
23
|
+
const files = execFileSync('git', ['ls-files', '-z'], { cwd: repo, encoding: 'utf8' }).split('\0').filter(Boolean);
|
|
24
|
+
const targets = files.filter(file => {
|
|
25
|
+
// Evidence and changelogs record historical observations, not current defaults.
|
|
26
|
+
if (/(^|\/)evidence\//.test(file) || /(^|\/)CHANGELOG[^/]*$/i.test(file)) return false;
|
|
27
|
+
return /^(apps|packages)\/.*\/(src|bin|test)\//.test(file)
|
|
28
|
+
|| /^(docs|skills)\//.test(file) || /(^|\/)README[^/]*$/i.test(file)
|
|
29
|
+
|| /(^|\/)package\.json$/.test(file);
|
|
30
|
+
});
|
|
31
|
+
assert.ok(targets.includes('packages/asset-resolver/src/env.mjs'));
|
|
32
|
+
assert.ok(targets.includes('apps/shell/extensions/akari-surfaces/src/browser/akari-settings-dialog.ts'));
|
|
33
|
+
const violations = targets.filter(file => !allowed.has(file) && readFileSync(path.join(repo, file), 'utf8').includes('akari-oss'));
|
|
34
|
+
assert.deepEqual(violations, [], 'Use the shared service URLs; only the legacy constant and this test may mention the retired domain');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('all official defaults use the current origin', () => {
|
|
38
|
+
assert.equal(LEGACY_AKARI_HOST, 'akari-oss.app');
|
|
39
|
+
assert.equal(DEFAULT_STORE_API, 'https://akari.video');
|
|
40
|
+
assert.equal(DEFAULT_STORE_BASE_URL, 'https://akari.video/api/store');
|
|
41
|
+
assert.equal(DEFAULT_STORE_LAB_BASE_URL, 'https://akari.video/lab');
|
|
42
|
+
assert.equal(DEFAULT_ASSETS_BASE_URL, 'https://akari.video/assets/');
|
|
43
|
+
assert.equal(DEFAULT_CATALOG_URL, 'https://akari.video/assets/catalog.json');
|
|
44
|
+
assert.equal(resolveEntitlementsUrl({}), 'https://akari.video/api/store/v1/entitlements');
|
|
45
|
+
assert.equal(resolveCatalogSource({}).value, DEFAULT_CATALOG_URL);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
for (const [input, expected] of [
|
|
49
|
+
[`https://${LEGACY_AKARI_HOST}/api/store`, 'https://akari.video/api/store'],
|
|
50
|
+
[`http://${LEGACY_AKARI_HOST}/assets/a?x=1#preview`, 'https://akari.video/assets/a?x=1#preview'],
|
|
51
|
+
[`https://${LEGACY_AKARI_HOST.toUpperCase()}/api/store/`, 'https://akari.video/api/store/'],
|
|
52
|
+
['https://akari.video/api/store', 'https://akari.video/api/store'],
|
|
53
|
+
['http://localhost:8788/api/store', 'http://localhost:8788/api/store'],
|
|
54
|
+
[`https://${LEGACY_AKARI_HOST}.evil.example/api/store`, `https://${LEGACY_AKARI_HOST}.evil.example/api/store`],
|
|
55
|
+
[`https://evil${LEGACY_AKARI_HOST}/api/store`, `https://evil${LEGACY_AKARI_HOST}/api/store`],
|
|
56
|
+
['not a URL', 'not a URL'],
|
|
57
|
+
['/local/assets/', '/local/assets/'],
|
|
58
|
+
]) test(`URL normalization: ${input}`, () => assert.equal(normalizeAkariUrl(input), expected));
|
|
59
|
+
|
|
60
|
+
for (const origin of [`https://${LEGACY_AKARI_HOST}`, 'https://akari.video', 'http://localhost:8788']) {
|
|
61
|
+
const expected = origin.includes(LEGACY_AKARI_HOST) ? 'https://akari.video' : origin;
|
|
62
|
+
test(`configured endpoints and saved credentials: ${origin}`, async t => {
|
|
63
|
+
const home = mkdtempSync(path.join(tmpdir(), 'akari-domain-'));
|
|
64
|
+
t.after(() => rmSync(home, { recursive: true, force: true }));
|
|
65
|
+
const credentials = { url: `${origin}/api/store/`, token: 'fixture-token' };
|
|
66
|
+
writeFileSync(path.join(home, 'store-credentials.json'), JSON.stringify(credentials));
|
|
67
|
+
const calls = [];
|
|
68
|
+
const result = await fetchEntitlements({ env: { AKARI_HOME: home }, fetchImpl: async url => {
|
|
69
|
+
calls.push(url);
|
|
70
|
+
return new Response(JSON.stringify({ entitlements: [{ product_id: 'example' }] }));
|
|
71
|
+
} });
|
|
72
|
+
assert.equal(result.status, 'ok');
|
|
73
|
+
assert.deepEqual([...result.ids], ['example']);
|
|
74
|
+
assert.deepEqual(calls, [`${expected}/api/store/v1/entitlements`]);
|
|
75
|
+
assert.equal(resolveEntitlementsUrl({ AKARI_STORE_API: `${origin}/` }, credentials), `${expected}/api/store/v1/entitlements`);
|
|
76
|
+
assert.equal(resolveDownloadUrl({}, credentials, 'a b'), `${expected}/api/store/v1/download/a%20b`);
|
|
77
|
+
assert.equal(resolveDownloadUrl({ AKARI_STORE_API: origin }, credentials, 'a b'), `${expected}/api/store/v1/download/a%20b`);
|
|
78
|
+
assert.equal(resolveCatalogSource({ AKARI_ASSETS_CATALOG: `${origin}/assets/catalog.json` }).value, `${expected}/assets/catalog.json`);
|
|
79
|
+
assert.equal(resolveEffectiveBase({}, { base: `${origin}/assets/` }), `${expected}/assets/`);
|
|
80
|
+
assert.equal(resolveEffectiveBase({ AKARI_ASSETS_BASE: `${origin}/assets/` }), `${expected}/assets/`);
|
|
81
|
+
assert.equal(deriveStoreLabBaseUrl(credentials.url), `${expected}/lab`);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const [layout, resolverPath, launcherPath] of [
|
|
86
|
+
['npm vendor', 'vendor/packages/asset-resolver/src', 'src'],
|
|
87
|
+
['Electron resources', 'packages/asset-resolver/src', 'packages/akari-launcher/src'],
|
|
88
|
+
]) test(`${layout}: resolver loads the canonical module from the installed launcher`, async t => {
|
|
89
|
+
const root = mkdtempSync(path.join(tmpdir(), 'akari-domain-vendor-'));
|
|
90
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
91
|
+
const resolverSrc = path.join(root, resolverPath);
|
|
92
|
+
const launcherSrc = path.join(root, launcherPath);
|
|
93
|
+
mkdirSync(resolverSrc, { recursive: true });
|
|
94
|
+
mkdirSync(launcherSrc, { recursive: true });
|
|
95
|
+
cpSync(path.join(repo, 'packages/akari-launcher/src/service-urls.cjs'), path.join(launcherSrc, 'service-urls.cjs'));
|
|
96
|
+
for (const file of ['service-urls.mjs', 'env.mjs']) cpSync(new URL(`../src/${file}`, import.meta.url), path.join(resolverSrc, file));
|
|
97
|
+
const installed = await import(pathToFileURL(path.join(resolverSrc, 'env.mjs')));
|
|
98
|
+
assert.equal(installed.resolveEntitlementsUrl({}, { url: `https://${LEGACY_AKARI_HOST}/api/store` }), 'https://akari.video/api/store/v1/entitlements');
|
|
99
|
+
assert.equal(installed.resolveCatalogSource({}).value, DEFAULT_CATALOG_URL);
|
|
100
|
+
});
|