spaps 0.8.2 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI_TOOLS.json +2410 -346
- package/README.md +89 -2
- package/assets/local-runtime/docker-compose.yml +2 -2
- package/client.js +4 -0
- package/package.json +1 -1
- package/src/ai-tool-spec.js +282 -19
- package/src/auth/handlers.js +109 -0
- package/src/auth/surface.js +653 -0
- package/src/cli-dispatcher.js +345 -1
- package/src/doctor.js +97 -5
- package/src/domain-cli.js +1 -0
- package/src/handlers.js +390 -1
- package/src/help-system.js +1 -1
- package/src/local-server.js +41 -4
package/src/handlers.js
CHANGED
|
@@ -509,9 +509,16 @@ function createHandlers(version, logo) {
|
|
|
509
509
|
},
|
|
510
510
|
doctor: async ({ options }) => {
|
|
511
511
|
const { runDoctor } = loadDoctor();
|
|
512
|
-
await runDoctor({
|
|
512
|
+
await runDoctor({
|
|
513
|
+
port: options.port || DEFAULT_PORT,
|
|
514
|
+
serverUrl: options.serverUrl || null,
|
|
515
|
+
origin: options.origin || null,
|
|
516
|
+
stripe: options.stripe || null,
|
|
517
|
+
json: options.json,
|
|
518
|
+
});
|
|
513
519
|
},
|
|
514
520
|
test: verifyHandler,
|
|
521
|
+
auth: async (...args) => loadAuthCommandHandlers().authHandler(...args),
|
|
515
522
|
login: async (...args) => loadAuthCommandHandlers().loginHandler(...args),
|
|
516
523
|
logout: async (...args) => loadAuthCommandHandlers().logoutHandler(...args),
|
|
517
524
|
whoami: async (...args) => loadAuthCommandHandlers().whoamiHandler(...args),
|
|
@@ -522,6 +529,11 @@ function createHandlers(version, logo) {
|
|
|
522
529
|
policy: policyHandler,
|
|
523
530
|
webhook: webhookHandler,
|
|
524
531
|
'issue-reports': issueReportsHandler,
|
|
532
|
+
access: capabilityHandler,
|
|
533
|
+
journey: capabilityHandler,
|
|
534
|
+
graph: capabilityHandler,
|
|
535
|
+
explain: capabilityHandler,
|
|
536
|
+
contract: capabilityHandler,
|
|
525
537
|
};
|
|
526
538
|
}
|
|
527
539
|
|
|
@@ -593,6 +605,250 @@ function assignIfDefined(target, key, value) {
|
|
|
593
605
|
}
|
|
594
606
|
}
|
|
595
607
|
|
|
608
|
+
function splitCsv(raw) {
|
|
609
|
+
if (!raw) return [];
|
|
610
|
+
return String(raw)
|
|
611
|
+
.split(',')
|
|
612
|
+
.map((value) => value.trim())
|
|
613
|
+
.filter(Boolean);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function extractErrorObject(result) {
|
|
617
|
+
const candidates = [result?.raw, result?.data];
|
|
618
|
+
for (const candidate of candidates) {
|
|
619
|
+
if (!candidate || typeof candidate !== 'object') continue;
|
|
620
|
+
if (candidate.error && typeof candidate.error === 'object') return candidate.error;
|
|
621
|
+
if (typeof candidate.code === 'string' || typeof candidate.message === 'string') {
|
|
622
|
+
return candidate;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function extractErrorMessage(result) {
|
|
629
|
+
const error = extractErrorObject(result);
|
|
630
|
+
return (
|
|
631
|
+
error?.message ||
|
|
632
|
+
result?.data?.detail ||
|
|
633
|
+
(result?.status ? `HTTP ${result.status}` : 'Request failed')
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function extractErrorCode(result) {
|
|
638
|
+
const error = extractErrorObject(result);
|
|
639
|
+
return error?.code || (result?.status ? `http_${result.status}` : 'request_failed');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function extractRequestId(result) {
|
|
643
|
+
const raw = result?.raw;
|
|
644
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
645
|
+
return raw.request_id || raw.metadata?.request_id || null;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function extractSources(data) {
|
|
649
|
+
if (Array.isArray(data?.sources)) return data.sources;
|
|
650
|
+
if (Array.isArray(data?.decision?.sources)) return data.decision.sources;
|
|
651
|
+
return [];
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function normalizeDiagnostic(item, fallbackStatus = null) {
|
|
655
|
+
const diagnostic = {
|
|
656
|
+
code: item?.code || (fallbackStatus ? `http_${fallbackStatus}` : 'request_failed'),
|
|
657
|
+
message: item?.message || 'Request failed',
|
|
658
|
+
status: item?.status ?? fallbackStatus,
|
|
659
|
+
};
|
|
660
|
+
assignIfDefined(diagnostic, 'details', item?.details);
|
|
661
|
+
return diagnostic;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function extractPayloadDiagnostics(result) {
|
|
665
|
+
const raw = result?.raw;
|
|
666
|
+
if (Array.isArray(raw?.diagnostics)) {
|
|
667
|
+
return raw.diagnostics.map((item) => normalizeDiagnostic(item, result?.status || null));
|
|
668
|
+
}
|
|
669
|
+
return [];
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function extractRemediations(data) {
|
|
673
|
+
const actions = Array.isArray(data?.next_actions) ? data.next_actions : [];
|
|
674
|
+
return actions.map((action) => {
|
|
675
|
+
const remediation = {
|
|
676
|
+
kind: action.kind,
|
|
677
|
+
label: action.label || null,
|
|
678
|
+
method: action.method || null,
|
|
679
|
+
path: action.path || null,
|
|
680
|
+
cli: action.cli || null,
|
|
681
|
+
requires: Array.isArray(action.requires) ? action.requires : [],
|
|
682
|
+
};
|
|
683
|
+
assignIfDefined(remediation, 'command_template', action.command_template);
|
|
684
|
+
assignIfDefined(remediation, 'safe_to_execute', action.safe_to_execute);
|
|
685
|
+
assignIfDefined(remediation, 'operator_gate_required', action.operator_gate_required);
|
|
686
|
+
return remediation;
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function normalizeRemediation(item) {
|
|
691
|
+
const remediation = {
|
|
692
|
+
kind: item.kind,
|
|
693
|
+
label: item.label || null,
|
|
694
|
+
method: item.method || null,
|
|
695
|
+
path: item.path || null,
|
|
696
|
+
cli: item.cli || null,
|
|
697
|
+
requires: Array.isArray(item.requires) ? item.requires : [],
|
|
698
|
+
};
|
|
699
|
+
assignIfDefined(remediation, 'details', item.details);
|
|
700
|
+
assignIfDefined(remediation, 'command_template', item.command_template);
|
|
701
|
+
assignIfDefined(remediation, 'safe_to_execute', item.safe_to_execute);
|
|
702
|
+
assignIfDefined(remediation, 'operator_gate_required', item.operator_gate_required);
|
|
703
|
+
return remediation;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function extractPayloadRemediations(result) {
|
|
707
|
+
const raw = result?.raw;
|
|
708
|
+
if (!Array.isArray(raw?.remediations)) return [];
|
|
709
|
+
return raw.remediations
|
|
710
|
+
.filter((item) => item && typeof item === 'object' && item.kind)
|
|
711
|
+
.map((item) => normalizeRemediation(item));
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function capabilityEnvelope({ command, result, data = null, success = null, diagnostics = [] }) {
|
|
715
|
+
const resolvedData = data === null && result ? result.data : data;
|
|
716
|
+
const ok = success === null ? Boolean(result?.ok) : Boolean(success);
|
|
717
|
+
const envelopeDiagnostics = Array.isArray(diagnostics) ? diagnostics.slice() : [];
|
|
718
|
+
envelopeDiagnostics.push(...extractPayloadDiagnostics(result));
|
|
719
|
+
if (result && !result.ok) {
|
|
720
|
+
const hasServerDiagnostic = envelopeDiagnostics.length > 0;
|
|
721
|
+
if (!hasServerDiagnostic) {
|
|
722
|
+
const error = extractErrorObject(result);
|
|
723
|
+
const diagnostic = {
|
|
724
|
+
code: extractErrorCode(result),
|
|
725
|
+
message: extractErrorMessage(result),
|
|
726
|
+
status: result.status || null,
|
|
727
|
+
};
|
|
728
|
+
assignIfDefined(diagnostic, 'details', error?.details);
|
|
729
|
+
envelopeDiagnostics.push(diagnostic);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const payloadRemediations = extractPayloadRemediations(result);
|
|
733
|
+
const envelope = {
|
|
734
|
+
schema_version: 'spaps.cli.capability.v1',
|
|
735
|
+
command,
|
|
736
|
+
success: ok,
|
|
737
|
+
status: result?.status || null,
|
|
738
|
+
data: resolvedData,
|
|
739
|
+
diagnostics: envelopeDiagnostics,
|
|
740
|
+
remediations: payloadRemediations.length ? payloadRemediations : extractRemediations(resolvedData),
|
|
741
|
+
sources: extractSources(resolvedData),
|
|
742
|
+
};
|
|
743
|
+
assignIfDefined(envelope, 'request_id', extractRequestId(result));
|
|
744
|
+
assignIfDefined(envelope, 'timestamp', result?.raw?.timestamp);
|
|
745
|
+
return envelope;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function emitCapability({ command, result, isJson, exitCodeOnFailure = 10 }) {
|
|
749
|
+
if (isJson) {
|
|
750
|
+
console.log(JSON.stringify(capabilityEnvelope({ command, result }), null, 2));
|
|
751
|
+
} else if (!result.ok) {
|
|
752
|
+
console.error(`\u2717 ${command}: ${extractErrorMessage(result)}`);
|
|
753
|
+
} else {
|
|
754
|
+
const data = result.data || {};
|
|
755
|
+
if (typeof data.allowed === 'boolean') {
|
|
756
|
+
console.log(`${command}: ${data.allowed ? 'allowed' : data.outcome || 'not allowed'}`);
|
|
757
|
+
(data.reasons || []).forEach((reason) => {
|
|
758
|
+
console.log(` - ${reason.message || reason.code}`);
|
|
759
|
+
});
|
|
760
|
+
} else if (typeof data.status === 'string' && typeof data.allowed === 'boolean') {
|
|
761
|
+
console.log(`${command}: ${data.status}`);
|
|
762
|
+
} else {
|
|
763
|
+
console.log(JSON.stringify(data, null, 2));
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (!result.ok) {
|
|
767
|
+
process.exitCode = exitCodeOnFailure;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function emitCapabilityError({ command, err, isJson, exitCode = 10 }) {
|
|
772
|
+
const message = err && err.message ? err.message : String(err);
|
|
773
|
+
const code = err && err.code ? err.code : 'ERROR';
|
|
774
|
+
if (isJson) {
|
|
775
|
+
console.log(JSON.stringify({
|
|
776
|
+
schema_version: 'spaps.cli.capability.v1',
|
|
777
|
+
command,
|
|
778
|
+
success: false,
|
|
779
|
+
status: err?.status || null,
|
|
780
|
+
data: null,
|
|
781
|
+
diagnostics: [{ code, message, status: err?.status || null }],
|
|
782
|
+
remediations: code === 'NOT_AUTHENTICATED' || code === 'SESSION_EXPIRED'
|
|
783
|
+
? [{ kind: 'authenticate', label: 'Run spaps login', cli: 'spaps login', requires: ['operator'] }]
|
|
784
|
+
: [],
|
|
785
|
+
sources: [],
|
|
786
|
+
}, null, 2));
|
|
787
|
+
} else {
|
|
788
|
+
const hint = code === 'NOT_AUTHENTICATED' || code === 'SESSION_EXPIRED'
|
|
789
|
+
? ' (run `spaps login` first)'
|
|
790
|
+
: '';
|
|
791
|
+
console.error(`\u2717 ${command}: ${message}${hint}`);
|
|
792
|
+
}
|
|
793
|
+
process.exitCode = exitCode;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function buildAccessDecisionPayload(options, prefix) {
|
|
797
|
+
if (!requireOption(options.action, `${prefix}: --action is required`)) return null;
|
|
798
|
+
if (!requireOption(options.resourceType, `${prefix}: --resource-type is required`)) return null;
|
|
799
|
+
if (!requireOption(options.resourceRef, `${prefix}: --resource-ref is required`)) return null;
|
|
800
|
+
|
|
801
|
+
const context = parseJsonFlag(options.context, `${prefix}: --context`);
|
|
802
|
+
if (!context.ok) return null;
|
|
803
|
+
const policyContext = parseJsonFlag(options.policyContext, `${prefix}: --policy-context`);
|
|
804
|
+
if (!policyContext.ok) return null;
|
|
805
|
+
const usageDimensions = parseJsonFlag(options.usageDimensions, `${prefix}: --usage-dimensions`);
|
|
806
|
+
if (!usageDimensions.ok) return null;
|
|
807
|
+
const authenticated = parseBooleanFlag(options.authenticated, `${prefix}: --authenticated`);
|
|
808
|
+
if (!authenticated.ok) return null;
|
|
809
|
+
|
|
810
|
+
const actor = {
|
|
811
|
+
actor_type: options.actorType || 'user',
|
|
812
|
+
};
|
|
813
|
+
assignIfDefined(actor, 'actor_ref', options.actorRef);
|
|
814
|
+
assignIfDefined(actor, 'user_id', options.userId);
|
|
815
|
+
assignIfDefined(actor, 'email', options.email);
|
|
816
|
+
assignIfDefined(actor, 'agent_id', options.agentId);
|
|
817
|
+
assignIfDefined(actor, 'authenticated', authenticated.value);
|
|
818
|
+
|
|
819
|
+
const resource = {
|
|
820
|
+
resource_type: options.resourceType,
|
|
821
|
+
resource_ref: options.resourceRef,
|
|
822
|
+
};
|
|
823
|
+
assignIfDefined(resource, 'resource_id', options.resourceId);
|
|
824
|
+
assignIfDefined(resource, 'resource_key', options.resourceKey);
|
|
825
|
+
|
|
826
|
+
const controls = {
|
|
827
|
+
approval_required: Boolean(options.approvalRequired),
|
|
828
|
+
authority_scope: options.authorityScope || 'application',
|
|
829
|
+
};
|
|
830
|
+
assignIfDefined(controls, 'entitlement_key', options.entitlementKey);
|
|
831
|
+
assignIfDefined(controls, 'entitlement_resource_type', options.entitlementResourceType);
|
|
832
|
+
assignIfDefined(controls, 'entitlement_resource_id', options.entitlementResourceId);
|
|
833
|
+
assignIfDefined(controls, 'policy_name', options.policyName);
|
|
834
|
+
assignIfDefined(controls, 'policy_context', policyContext.value);
|
|
835
|
+
assignIfDefined(controls, 'usage_feature_key', options.usageFeatureKey);
|
|
836
|
+
assignIfDefined(controls, 'usage_dimensions', usageDimensions.value);
|
|
837
|
+
assignIfDefined(controls, 'x402_resource_key', options.x402ResourceKey);
|
|
838
|
+
assignIfDefined(controls, 'approval_id', options.approvalId);
|
|
839
|
+
|
|
840
|
+
const payload = {
|
|
841
|
+
actor,
|
|
842
|
+
action: options.action,
|
|
843
|
+
resource,
|
|
844
|
+
controls,
|
|
845
|
+
context: context.value || {},
|
|
846
|
+
};
|
|
847
|
+
assignIfDefined(payload, 'idempotency_key', options.idempotencyKey);
|
|
848
|
+
assignIfDefined(payload, 'correlation_id', options.correlationId);
|
|
849
|
+
return payload;
|
|
850
|
+
}
|
|
851
|
+
|
|
596
852
|
async function dayrateHandler({ options }) {
|
|
597
853
|
const { callEndpoint, emit, emitAuthError } = loadDomainCli();
|
|
598
854
|
const isJson = Boolean(options.json);
|
|
@@ -983,4 +1239,137 @@ async function issueReportsHandler({ options }) {
|
|
|
983
1239
|
}
|
|
984
1240
|
}
|
|
985
1241
|
|
|
1242
|
+
async function capabilityHandler({ name, options }) {
|
|
1243
|
+
const { callEndpoint } = loadDomainCli();
|
|
1244
|
+
const isJson = Boolean(options.json);
|
|
1245
|
+
const command = name === 'access'
|
|
1246
|
+
? `access.${options.subcommand || 'unknown'}`
|
|
1247
|
+
: name === 'journey'
|
|
1248
|
+
? `journey.${options.subcommand || 'unknown'}`
|
|
1249
|
+
: name === 'graph'
|
|
1250
|
+
? `graph.${options.subcommand || 'unknown'}`
|
|
1251
|
+
: name;
|
|
1252
|
+
|
|
1253
|
+
try {
|
|
1254
|
+
if (name === 'access') {
|
|
1255
|
+
if (options.subcommand !== 'check') {
|
|
1256
|
+
console.error('access: unknown subcommand. Supported: check');
|
|
1257
|
+
process.exitCode = 2;
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
const body = buildAccessDecisionPayload(options, 'access check');
|
|
1261
|
+
if (!body) return;
|
|
1262
|
+
const result = await callEndpoint({
|
|
1263
|
+
options,
|
|
1264
|
+
method: 'POST',
|
|
1265
|
+
path: '/api/access/decide',
|
|
1266
|
+
body,
|
|
1267
|
+
});
|
|
1268
|
+
emitCapability({ command, result, isJson, exitCodeOnFailure: 10 });
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
if (name === 'journey') {
|
|
1273
|
+
if (options.subcommand !== 'run') {
|
|
1274
|
+
console.error('journey: unknown subcommand. Supported: run');
|
|
1275
|
+
process.exitCode = 2;
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
const access = buildAccessDecisionPayload(options, 'journey run');
|
|
1279
|
+
if (!access) return;
|
|
1280
|
+
const operatorLabels = splitCsv(options.operatorLabels)
|
|
1281
|
+
.filter((label) => label.toLowerCase() !== 'operator-gated');
|
|
1282
|
+
const body = {
|
|
1283
|
+
access,
|
|
1284
|
+
include_command_templates: Boolean(options.includeCommandTemplates),
|
|
1285
|
+
operator_labels: operatorLabels,
|
|
1286
|
+
environment: options.environment || 'production',
|
|
1287
|
+
};
|
|
1288
|
+
const result = await callEndpoint({
|
|
1289
|
+
options,
|
|
1290
|
+
method: 'POST',
|
|
1291
|
+
path: '/api/actions/prepare',
|
|
1292
|
+
body,
|
|
1293
|
+
});
|
|
1294
|
+
emitCapability({ command, result, isJson, exitCodeOnFailure: 30 });
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
if (name === 'graph') {
|
|
1299
|
+
let result;
|
|
1300
|
+
if (options.subcommand === 'nodes') {
|
|
1301
|
+
const query = {};
|
|
1302
|
+
assignIfDefined(query, 'node_type', options.nodeType);
|
|
1303
|
+
assignIfDefined(query, 'status', options.status);
|
|
1304
|
+
assignIfDefined(query, 'q', options.query);
|
|
1305
|
+
assignIfDefined(query, 'cursor', options.cursor);
|
|
1306
|
+
assignIfDefined(query, 'limit', options.limit);
|
|
1307
|
+
assignIfDefined(query, 'application_id', options.applicationId);
|
|
1308
|
+
result = await callEndpoint({ options, method: 'GET', path: '/api/graph/nodes', query });
|
|
1309
|
+
} else if (options.subcommand === 'paths') {
|
|
1310
|
+
const query = {
|
|
1311
|
+
from_node_key: options.fromNodeKey,
|
|
1312
|
+
to_node_key: options.toNodeKey,
|
|
1313
|
+
};
|
|
1314
|
+
assignIfDefined(query, 'max_depth', options.maxDepth);
|
|
1315
|
+
assignIfDefined(query, 'limit', options.limit);
|
|
1316
|
+
assignIfDefined(query, 'include_stale', options.includeStale);
|
|
1317
|
+
assignIfDefined(query, 'application_id', options.applicationId);
|
|
1318
|
+
result = await callEndpoint({ options, method: 'GET', path: '/api/graph/paths', query });
|
|
1319
|
+
} else if (options.subcommand === 'impact') {
|
|
1320
|
+
const query = {
|
|
1321
|
+
node_key: options.nodeKey,
|
|
1322
|
+
};
|
|
1323
|
+
assignIfDefined(query, 'max_depth', options.maxDepth);
|
|
1324
|
+
assignIfDefined(query, 'limit', options.limit);
|
|
1325
|
+
assignIfDefined(query, 'include_stale', options.includeStale);
|
|
1326
|
+
assignIfDefined(query, 'application_id', options.applicationId);
|
|
1327
|
+
result = await callEndpoint({ options, method: 'GET', path: '/api/graph/impact', query });
|
|
1328
|
+
} else if (options.subcommand === 'refresh') {
|
|
1329
|
+
const query = {};
|
|
1330
|
+
assignIfDefined(query, 'application_id', options.applicationId);
|
|
1331
|
+
assignIfDefined(query, 'correlation_id', options.correlationId);
|
|
1332
|
+
result = await callEndpoint({ options, method: 'POST', path: '/api/graph/refresh', query });
|
|
1333
|
+
} else {
|
|
1334
|
+
console.error('graph: unknown subcommand. Supported: nodes, paths, impact, refresh');
|
|
1335
|
+
process.exitCode = 2;
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
emitCapability({ command, result, isJson, exitCodeOnFailure: 10 });
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
if (name === 'explain') {
|
|
1343
|
+
if (!requireOption(options.decisionId, 'explain: <decision-id> is required')) return;
|
|
1344
|
+
const result = await callEndpoint({
|
|
1345
|
+
options,
|
|
1346
|
+
method: 'GET',
|
|
1347
|
+
path: `/api/graph/explain/${encodeURIComponent(options.decisionId)}`,
|
|
1348
|
+
});
|
|
1349
|
+
emitCapability({ command, result, isJson, exitCodeOnFailure: 10 });
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
if (name === 'contract') {
|
|
1354
|
+
const result = await callEndpoint({
|
|
1355
|
+
options,
|
|
1356
|
+
method: 'GET',
|
|
1357
|
+
path: '/api/contract',
|
|
1358
|
+
});
|
|
1359
|
+
emitCapability({ command, result, isJson, exitCodeOnFailure: 20 });
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
console.error('capability: unsupported command');
|
|
1364
|
+
process.exitCode = 2;
|
|
1365
|
+
} catch (err) {
|
|
1366
|
+
emitCapabilityError({
|
|
1367
|
+
command,
|
|
1368
|
+
err,
|
|
1369
|
+
isJson,
|
|
1370
|
+
exitCode: name === 'contract' ? 20 : name === 'journey' ? 30 : 10,
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
986
1375
|
module.exports = { createHandlers };
|
package/src/help-system.js
CHANGED
|
@@ -236,7 +236,7 @@ const HELP_TREE = {
|
|
|
236
236
|
SPAPS (Sweet Potato Authentication & Payment Service) provides:
|
|
237
237
|
|
|
238
238
|
1. **Authentication**: Email/password, magic links, and wallet-based auth
|
|
239
|
-
2. **Payments**: Stripe subscriptions and
|
|
239
|
+
2. **Payments**: Stripe subscriptions and x402 paid-resource receipts
|
|
240
240
|
3. **Local Development**: Zero-config local mode with auto-auth
|
|
241
241
|
4. **Type Safety**: Full TypeScript support with generated types
|
|
242
242
|
5. **Multi-tenant**: API key-based app isolation
|
package/src/local-server.js
CHANGED
|
@@ -12,6 +12,7 @@ const axios = require('axios');
|
|
|
12
12
|
const os = require('os');
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const fs = require('fs');
|
|
15
|
+
const crypto = require('crypto');
|
|
15
16
|
|
|
16
17
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
17
18
|
const BUNDLED_RUNTIME_ROOT = path.join(PACKAGE_ROOT, 'assets', 'local-runtime');
|
|
@@ -127,11 +128,42 @@ function ensureBundledRuntime(runtimeDir) {
|
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
function
|
|
131
|
+
function readOrCreateRuntimeSecret(runtimeDir, envName, fileName) {
|
|
132
|
+
const envValue = process.env[envName];
|
|
133
|
+
if (envValue) {
|
|
134
|
+
return envValue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const secretPath = path.join(runtimeDir, fileName);
|
|
138
|
+
if (fs.existsSync(secretPath)) {
|
|
139
|
+
const existing = fs.readFileSync(secretPath, 'utf8').trim();
|
|
140
|
+
if (existing) {
|
|
141
|
+
return existing;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
146
|
+
const generated = crypto.randomBytes(32).toString('hex');
|
|
147
|
+
fs.writeFileSync(secretPath, `${generated}\n`, { mode: 0o600 });
|
|
148
|
+
fs.chmodSync(secretPath, 0o600);
|
|
149
|
+
return generated;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function buildComposeEnv({ port, authBaseUrl, runtimeDir = null, runtimeMode = 'repo' }) {
|
|
131
153
|
const manifest = readBundledRuntimeManifest();
|
|
132
154
|
const portableRuntime = manifest.portable_runtime || {};
|
|
133
|
-
|
|
155
|
+
const composeEnv = {
|
|
134
156
|
...process.env,
|
|
157
|
+
};
|
|
158
|
+
if (runtimeMode === 'bundle' && runtimeDir) {
|
|
159
|
+
composeEnv.SPAPS_LOCAL_POSTGRES_PASSWORD = readOrCreateRuntimeSecret(
|
|
160
|
+
runtimeDir,
|
|
161
|
+
'SPAPS_LOCAL_POSTGRES_PASSWORD',
|
|
162
|
+
'.spaps-local-postgres-password'
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
...composeEnv,
|
|
135
167
|
SPAPS_LOCAL_PORT: String(port),
|
|
136
168
|
SPAPS_LOCAL_MODE: String(process.env.SPAPS_LOCAL_MODE ?? portableRuntime.default_local_mode ?? true),
|
|
137
169
|
SPAPS_AUTH_BASE_URL:
|
|
@@ -177,7 +209,7 @@ function resolveLocalRuntime({
|
|
|
177
209
|
runtimeDir: repoRoot,
|
|
178
210
|
cwd: repoRoot,
|
|
179
211
|
composeFile: path.join(repoRoot, 'docker-compose.spaps-dev.yml'),
|
|
180
|
-
composeEnv: buildComposeEnv({ port, authBaseUrl }),
|
|
212
|
+
composeEnv: buildComposeEnv({ port, authBaseUrl, runtimeDir: repoRoot, runtimeMode: 'repo' }),
|
|
181
213
|
projectName: path.basename(repoRoot),
|
|
182
214
|
};
|
|
183
215
|
}
|
|
@@ -192,7 +224,12 @@ function resolveLocalRuntime({
|
|
|
192
224
|
runtimeDir: resolvedRuntimeDir,
|
|
193
225
|
cwd: resolvedRuntimeDir,
|
|
194
226
|
composeFile: path.join(resolvedRuntimeDir, 'docker-compose.yml'),
|
|
195
|
-
composeEnv: buildComposeEnv({
|
|
227
|
+
composeEnv: buildComposeEnv({
|
|
228
|
+
port,
|
|
229
|
+
authBaseUrl,
|
|
230
|
+
runtimeDir: resolvedRuntimeDir,
|
|
231
|
+
runtimeMode: 'bundle',
|
|
232
|
+
}),
|
|
196
233
|
projectName: `spaps-local-${port}`,
|
|
197
234
|
};
|
|
198
235
|
}
|